Представь себе следующую игровую ситуацию: воздух на огромной скорости рассекает пущенная из рокет-лончера ракета, за ней тянется дымовой след, ракета врезается в стену и разрывается на тысячи ослепительных осколков, при этом одному из игроков посчастливилось быть в этот момент рядом — его тело с хлюпаньем разлетается на множество кусков, и кровища забрызгивает все стены и пол помещения. Знаешь, чего общего есть во всех этих оптимистичных спецэффектах? А дело все в том, что и дым, и искры, и кровища делаются с помощью систем частиц (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))

Похоже это все.

Оставить мнение

Check Also

Windows 10 против шифровальщиков. Как устроена защита в обновленной Windows 10

Этой осенью Windows 10 обновилась до версии 1709 с кодовым названием Fall Creators Update …