Представь себе следующую игровую ситуацию: воздух на огромной скорости рассекает пущенная из рокет-лончера ракета, за ней тянется дымовой след, ракета врезается в стену и разрывается на тысячи ослепительных осколков, при этом одному из игроков посчастливилось быть в этот момент рядом - его тело с хлюпаньем разлетается на множество кусков, и кровища забрызгивает все стены и пол помещения. Знаешь, чего общего есть во всех этих оптимистичных спецэффектах? А дело все в том, что и дым, и искры, и кровища делаются с помощью систем частиц (particle systems), которыми мы сейчас и займемся.
Качественная реализация системы частиц займет не одну сотню строк кода, поэтому первое, о чем стоит хорошенько подумать, так это организация структур данных и оптимальный менеджмент памяти.
Всегда следует помнить о том, что частицы существенно увеличивают количество полигонов в сцене. Так на рендеринг 2000 снежинок уйдет около 4000 треугольников, причем из-за того, что положение снежинок каждый кадр будет меняться, мы не сможем воспользоваться статичным массивом вершин, чтобы запихать все данные в буфер видеокарточки.
Для хранения информации о частице можно воспользоваться следующей структурой:
typedef struct particle_s{
particle_s *next, *prev;
int startTime;
int endTime;
int frame
vec3_t pos;
vec3_t oldpos;
vec3_t velocity;
vec4_t color;
float size;
} particle_t;
particle_s *next, *prev: Это указатели на следующую и предыдущую частицы в списке.
int startTime: Это время - начало жизни частицы.
int endTime: Это время, когда частица умрет (например, когда капля дождя достигнет земли).
int frame: Номер текущего кадра для анимированных частиц.
vec3_t pos: Текущие координаты (x, y, z) частицы.
vec3_t oldpos: Предыдущие координаты (требуются для реализации частиц с эффектом motion blur).
vec3_t velocity: Вектор скорости частицы.
vec4_t color: Цвет частицы (RBGA).
float size: Текущий размер частицы.
Это основные характеристики частицы, с помощью которых можно описать самые различные эффекты.
Ввиду того, что я не являюсь поклонником использования методов объектно-ориентированного программирования при создании игр (еще не хватало, чтоб игра по устройству смахивала на любимую ОС), я не стану описывать организацию специального класса для системы частиц (ты сможешь сделать это сам, если будет нужно), а ограничусь описанием небольшой структуры, для хранения информации о целой системе частиц:
typedef struct {
int type;
vec3_t origin;
int lastEmit;
particle_t particles[MAX_PARTICLES];
particle_t activeParticles;
particle_t *freeParticles;
} particleSystem_t;
int type: Это тип системы частиц, по которому мы определим, что нам с этими частицами делать.
vec3_t origin: Координаты источника частиц.
int lastEmit: Время, когда была выпущена последняя частица.
particle_t particles[MAX_PARTICLES]: Зарезервированный массив частиц. Нужен, чтобы лишний раз не обращаться к функция выделения/освобождения памяти.
particle_t activeParticles: Связанный список частиц.
particle_t *freeParticles: Указатель на список не занятых частиц.
Весь процесс управления системой частиц сводится к следующему. В самом начале работы, выделяем под все частицы память с таким расчетом, чтобы ее хватило на максимальное число частиц. Затем, каждый кадр, в зависимости от типа системы частиц, соответствующим образом обновляем состояние всех частиц (двигаем их куда нужно, меняем цвет, размер и т.п.). Когда все закончится, освобождаем память.
Для пущей крутизны можешь написать поддержку подключаемых систем частиц, в виде динамических библиотек DLL.
Рендеринг частиц
Скорее всего, тебе захочется нарисовать частицы в виде красивеньких спрайтов, а не в виде дурацких точек, по типу тех, что мы могли наблюдать в первых двух Quake'ах, поэтому стоит уделить этому некоторое внимание.
Последний DirectX очень неплохо приспособлен для рендеринга плоских спрайтов в 3D сценах, а вот у почитателей OpenGL могут возникнуть некоторые проблемы. Дело в том, что OpenGL не поддерживает так называемые bilboard'ы, поэтому приходится идти на некоторые ухищрения.
Чтобы нарисовать в OpenGL спрайт или частицу, нам нужно добавить в 3D сцену полигон и ориентировать его так, чтобы он был перпендикулярен направлению взгляда, т.е. чтобы его нормаль совпадала по направлению с вектором, задающим ориентацию камеры в пространстве. Делается это довольно просто, если заранее знать три ортогональных вектора (вперед - Z, вправо - X и вверх - Y) задающих ориентацию камеры в пространстве.
Если ты задаешь ориентацию камеры с помощью трех углов вращения вокруг соответствующих осей, то из них можно легко извлечь нужные направляющие векторы с помощью следующей функции:
/*
================
AnglesToAxis
Angles - три угла вращения.
Axis[3] - три вектора (вправо, вверх, вперед)
================
*/
void AnglesToAxis (vec3_t angles, vec3_t axis[3])
{
float angle;
static float sx, sy, sz, cx, cy, cz;
// переводим первый угол в радианы и ищем синус и косинус
angle = angles[0] * (M_PI*2 / 360);
sx = sin(angle);
cx = cos(angle);
// переводим второй угол в радианы и ищем синус и косинус
angle = angles[1] * (M_PI*2 / 360);
sy = sin(angle);
cy = cos(angle);
// переводим третий угол в радианы и ищем синус и косинус
angle = angles[2] * (M_PI*2 / 360);
sz = sin(angle);
cz = cos(angle);
// находим координаты нужных векторов
axis[0][0] = cy*cz;
axis[0][1] = -cy*sz;
axis[0][2] = sy;
axis[1][0] = sx*sy*cz + cx*sz;
axis[1][1] = cx*cz - sx*sy*sz;
axis[1][2] = -sx*cy;
axis[2][0] = sx*sz - cx*sy*cz;
axis[2][1] = cx*sy*sz + sx*cz;
axis[2][2] = cx*cy;
}
Программная реализация
Для того, чтобы начать работать с системой частиц, ее нужно инициализировать с помощью функции InitParticles (), эта функция отчищает нужную память и инициализирует связанный список частиц. Затем, чтобы добавить новую частицу мы вызываем функцию AllocParticle (), а чтобы удалить ненужную частицу FreeParticle (). Заметь, что все эти функции реализованы без использования процедур менеджмента памяти: malloc(), free() и т.п. что значительно увеличивает скорость, и все благодаря аккуратно реализованным спискам.
// инициализируем список частиц системы ps
void InitParticles(particleSystem_t *ps)
{
int i;
// отчищаем память
memset( ps->particles, 0, sizeof( ps->particles ) );
// инициализируем список
ps->activeParticles.next = &ps->activeParticles;
ps->activeParticles.prev = &ps->activeParticles;
// пока все частицы свободны
ps->freeParticles = ps->particles;
// связываем частицы
for ( i = 0 ; i < MAX_PARTICLES - 1 ; i++ )
ps->particles[i].next = &ps->particles[i+1];
}
// Функция удаляет частицу p из списка частиц системы ps
void FreeParticle (particleSystem_t *ps, particle_t *p)
{
// удаляем из списка
p->prev->next = p->next;
p->next->prev = p->prev;
// и переносим ее в список свободных частиц
p->next = ps->freeParticles;
ps->freeParticles = p;
}
// Функция создает новую частицу в системе ps и возвращает на нее указатель
particle_t *AllocParticle(particleSystem_t *ps)
{
particle_t *p;
// если нет свободных частиц, то давим самую первую
if ( !ps->freeParticles )
FreeParticle (ps, ps->activeParticles.prev);
p = ps->freeParticles;
ps->freeParticles = ps->freeParticles->next;
// отчищаем поля структуры
memset( p, 0, sizeof( *p ) );
// добавляем частицу в список
p->next = ps->activeParticles.next;
p->prev = &ps->activeParticles;
ps->activeParticles.next->prev = p;
ps->activeParticles.next = p;
// возвращаем указатель
return p;
}
Теперь, допустим, что мы хотим реализовать систему частиц, которая будет имитировать дымок от костра, такие плавно поднимающиеся частицы, которые увеличиваются в размерах и постепенно становятся полностью непрозрачными. Для этого нужно вызвать функцию, которая запустит систему частиц. В функцию мы передаем тип системы - type и координаты источника частиц - origin.
void InitParticleSystem (particleSystem_t *ps, int type, vec3_t origin)
{
// копируем тип системы частиц
ps->type = type;
// и координаты источника
ps->origin = origin;
ps->lastEmit = timeGetTime();
/* Теперь в зависимости от типа системы частиц можно сделать те, или иные приготовительные действия. Нам пока ничего не надо. */
}
После того, как мы запустили систему нам нужно каждый кадр обновлять состояние всех частиц, поэтому используем функцию UpgradeParticleSystem (), которая и сделает все необходимое.
void UpgradeParticleSystem (particleSystem_t *ps)
{
particle_t *p, *next; // вспомогательные указатели
p = ps->activeParticles.next;
// выходим если список частиц пуст
if (p == &ps->activeParticles)
return;
// в зависимости от типа системы делаем нужные изменения
switch (ps->type) {
case PS_CAMPFIRE_SMOKE: // дым от костра
// каждые 50 миллисекунд создаем новую частицу
if (timeGetTime() > ps->lastEmit + 50) {
p = R_AllocParticle (ps); // создаем частицу
p->startTime = timeGetTime (); // запоминаем текущее время
p->endTime = p->startTime + 2000; // время жизни 2 сек.
VectorCopy (ps->origin, p->origin); // начальные координаты
p->size = 2.0; // размер частицы
p->color[0] = p->color[1] = p->color[2] = p->color[3] = 1;
ps->lastEmit = p->startTime;
}
// пробигаемся по списку частиц
for ( ; p!=&ps->activeParticles ; p = next) {
next = p->next;
// если время частицы вышло, то давим ее
if (p->endTime <= timeGetTime()) {
FreeParticle (ps, p);
continue;
}
// частица поднимается вверх.
/* frametime - это длительность предыдущего кадра в секундах, для избежания зависимости скорости движения частиц от скорости компа */
p->origin[2] += frametime * 40;
// частица увеличивается в диаметре
p->size += frametime*4;
// и становится прозрачной по линейной зависимости
p->color[3] = 0.6*(1.0 - (timeGetTime() - p->startTime)/2000.0);
}
break;
}
}
Ну и наконец, нам нужно нарисовать наши частицы в виде спрайтов. Для этого нам потребуются три вектора ориентирующих камеру в пространстве - viewaxis[3], про которые я говорил выше.
void DrawParticles (particleSystem_t *ps)
{
particle_t *p, *next;
vec3_t v;
/*
Сначала нужно установить нужные функции блендинга (glBlendFunc) и сделать текущей нужную текстуру (glBindTexture).
*/
// Рисуем квадратные спрайты
glBegin (GL_QUADS);
// пробегаемся по списку частиц
p = ps->activeParticles.next;
for ( ; p!=&ps->activeParticles ; p = next) {
next = p->next;
// цвет частицы
glColor4fv (p->color);
// первый вертекс
VectorMA(p->origin, -p->size, viewaxis[0], v);
VectorMA(v, -p->size, viewaxis[1], v);
glTexCoord2f (0, 0);
glVertex3fv (v);
// второй вертекс
VectorMA(p->origin, p->size, viewaxis[0], v);
VectorMA(v, -p->size, viewaxis[1], v);
glTexCoord2f (1.0, 0);
glVertex3fv (v);
// третий вертекс
VectorMA(p->origin, p->size, viewaxis[0], v);
VectorMA(v, p->size, viewaxis[1], v);
glTexCoord2f (1.0, 1.0);
glVertex3fv (v);
// четвертый вертекс
VectorMA(p->origin, -p->size, viewaxis[0], v);
VectorMA(v, p->size, viewaxis[1], v);
glTexCoord2f (0, 1.0);
glVertex3fv (v);
}
// закончили
glEnd ();
}
Макрос VectorMA () выглядит следующим образом:
#define VectorMA(v, s, b, o) ((o)[0]=(v)[0]+(b)[0]*(s),(o)[1]=(v)[1]+(b)[1]*(s),(o)[2]=(v)[2]+(b)[2]*(s))
Похоже это все.