Частицы и системы частиц — распространённая вещь, которую вы можете увидеть в любой игре.

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

Итак, что составляет частицу? У вас может быть много разных переменных, но вот те, что мы будем использовать:

Position — содержит мировые координаты центра частицы.
Velocity — содержит направление и скорость, с которой частица двигается.
Color — 32-битный цвет частицы.
Size — ширина и высота квадрата частицы
Life — длительность времени в секундах, пока частица активна (видна)
Angle — угол вращения UV-координат частицы
Texture — класс, сохраняющий текстуру

Также на все частицы влияет гравитация. Одна и та же на все частицы.

Чтобы продемонстрировать работу частиц, я создал эффект пламени. Оранжевый цвет, используемый для эффекта: R=215; G=115; B=40.

Ещё не всё понятно? Ну, тогда я могу только посоветовать попробовать разобраться в процессе написания кода…

Исходный код взят из урока «Загрузка текстур». Добавлены файлы vector.h и vector.cpp, а так же новые файлы particle.h и particle.cpp — класс частиц.

Файл vector.h — заголовочный файл класса CVector:

#ifndef VECTOR_H
#define VECTOR_H#include <math.h>class CVector
{
public:// Конструкторы
CVector() : x(0.0f), y(0.0f), z(0.0f) {}
CVector(float xxx, float yyy, float zzz) : x(xxx), y(yyy), z(zzz) {}// Копирующий конструктор
CVector(const CVector &vec) : x(vec.x), y(vec.y), z(vec.z) {}

// Перегружаем операторы
CVector& operator =(const CVector &vec);
CVector operator +(const CVector &vec) const;
CVector operator (const CVector &vec) const;
CVector operator () const;
void operator +=(const CVector &vec);
void operator -=(const CVector &vec);
bool operator ==(const CVector &vec) const;
bool operator !=(const CVector &vec) const;

float operator *(const CVector &vec) const;

// Устанавливает вектор в переданные переменные
void set(float xxx, float yyy, float zzz);
void set(const CVector &vec);

void negate(); // Вычитает вектор
void normalize(); // Нормализует вектор
void scale(float amt); // Масштабирует вектор на переданные значения

float magnitude() const; // Возвращает величину вектора

// Cross
void crossProduct(const CVector &vec, CVector &result) const;
CVector crossProduct(const CVector &vec) const;

// Public data
float x;
float y;
float z;
};

// Typedef
typedef CVector CPos;

#endif

 

Файл vector.cpp — реализация класса вектор:

#include <stdlib.h>
#include <assert.h>
#include «vector.h»inline bool TEqual(float a, float b, float t)
{
return ((a > b t) && (a < b + t));
}// Оператор присваивания
CVector& CVector::operator =(const CVector &vec)
{
x = vec.x;
y = vec.y;
z = vec.z;
return *this;
}// Оператор сложения
CVector CVector::operator +(const CVector &vec) const
{
return CVector(x + vec.x, y + vec.y, z + vec.z);
}// Оператор вычитания
CVector CVector::operator (const CVector &vec) const
{
return CVector(x vec.x, y vec.y, z vec.z);
}

// Противоположное значение
CVector CVector::operator () const
{
return CVector(x, y, z);
}

// +=оператор
void CVector::operator +=(const CVector &vec)
{
x += vec.x;
y += vec.y;
z += vec.z;
}

// -= оператор
void CVector::operator -=(const CVector &vec)
{
x -= vec.x;
y -= vec.y;
z -= vec.z;
}

// Оператор сравнения
bool CVector::operator ==(const CVector &vec) const
{
return (TEqual(x, vec.x, .001f) &&
TEqual(y, vec.y, .001f) &&
TEqual(z, vec.z, .001f));
}

// Оператор !=
bool CVector::operator !=(const CVector &vec) const
{
return !(*this == vec);
}

// Умножение
float CVector::operator *(const CVector &vec) const
{
return (x * vec.x + y * vec.y + z * vec.z);
}

// Установить вектор в переданные позиции x,y,z
void CVector::set(float xxx, float yyy, float zzz)
{
x = xxx;
y = yyy;
z = zzz;
}

// Устанавливает вектор в позиции переданного CVector
void CVector::set(const CVector &vec)
{
x = vec.x;
y = vec.y;
z = vec.z;
}

// делает вектор отрицательным
void CVector::negate()
{
x = x;
y = y;
z = z;
}

// Нормализует вектор
void CVector::normalize()
{
assert(!TEqual(magnitude(), 0.0f, .001f)); // убедимся, что длинна не 0

float oneOverLen = 1.0f / magnitude();

x *= oneOverLen;
y *= oneOverLen;
z *= oneOverLen;
}

// Масштабирует вектор на переданное значение
void CVector::scale(float amt)
{
x *= amt;
y *= amt;
z *= amt;
}

// возвращает величину
float CVector::magnitude() const
{
return sqrtf((x * x) + (y * y) + (z * z));
}

// находит  cross
void CVector::crossProduct(const CVector &vec, CVector &result) const
{
result.x = (y * vec.z) (vec.y * z);
result.y = (z * vec.x) (vec.z * x);
result.z = (x * vec.y) (vec.x * y);
}

// тоже находит cross
CVector CVector::crossProduct(const CVector &vec) const
{
return CVector((y * vec.z) (vec.y * z),
(z * vec.x) (vec.z * x),
(x * vec.y) (vec.x * y));
}

 

Теперь создадим новый класс, класс частиц. Опишем его в файлах particle.h и particle.cpp

Хидер-файл класса частиц, particle.h:

#ifndef PARTICLE_H
#define PARTICLE_H#include <stdio.h>
#include «main.h»
#include «vector.h»// Создаёт ARGB-цвет:
#define ARGB(A, R, G, B) ( (int)((A & 0xFF) << 24 |
(R & 0xFF) << 16 |
(G & 0xFF) << 8 |
(B & 0xFF)) )// Функции получают значения A,R,G и B из ARGB-цвета:
#define GET_A(c) ((c >> 24) & 0xFF)
#define GET_R(c) ((c >> 16) & 0xFF)
#define GET_G(c) ((c >> 8) & 0xFF)
#define GET_B(c) (c & 0xFF)// Возвращает рандомный процент между 0 и 1:
#define RAND_PERCENT() ((rand() & 0x7FFF) / ((float)0x7FFF))

// Возвращает рандомное значение между (и включая) «min» и «max»
// Естественно, «min» < «max»
#define RAND(min, max) (min + (max — min) * RAND_PERCENT())

// Гравитация, воздействующая на каждую частицу. Значение
// получено методом «как лучше выглядит» %)
const float kParticleGravity = 9.81f / 10.0f;

// Частица
class CParticle
{
public:

CParticle(); // Конструктор по умолчанию

// Функция инициализирует частицы:
bool init(const CPos &pos, const CVector &vel, float lifeSpan, float size,
float angle = 0.0f, int color = 0xFFFFFFFF, GLuint TID=-1);

void process(float dt); // Двигает частицы каждый кадр
void render(); // Выводит частицы на экран

// Если значение «жизнь» частицы больше 0, она активна, иначе — нет
bool isAlive() { return (life > 0.0f); }

private:

CPos pos; // Позиция в мире
CVector vel; // Скорость

int color; // ARGB цвет частицы
float size; // Размеры частицы
float life; // Время жизни частицы в секундах
float angle; // Угол в градусах для вращения текстурных коорд-т каждую секунду
float angleNow; // Текущий угол вращения текстуры

GLuint textureID;   // Текстура частицы
};

#endif

 

Вот и весь класс частиц. Что он делает? Создает частицы, накладывает на них текстуры, запускает жизненный цикл, выводит на экран, подчищает умершие, и т.д.

Теперь напишем реализацию класса.

Файл particle.cpp:

#include «particle.h»

// Конструктор по умолчанию — обнуляет всё
CParticle::CParticle()
{
color = ARGB(255, 255, 255, 255);
size = 0.0f;
life = 0.0f;
angle = 0.0f;

}

// Инициализация частиц:
bool CParticle::init(const CPos &p, const CVector &v, float lifeSpan, float s, float a, int c,
GLuint TID)
{

pos = p; // Установим позицию
vel = v; // Установим скорость

// Не допустим инициализацию «мертвой» частицы
if(lifeSpan <= 0.0f)
return false;

life = lifeSpan; // Установим время жизни частицы в секундах

// Размер должен быть положительным
if(s <= 0.0f)
return false;

size = s; // Установим размер
angle = a; // Установим угол вращения UV координат
color = c; // Установим цвет

this->textureID = TID;
return true;
}

void CParticle::process(float dt)
{
// Если частица мертва, сбросим её позицию
if(isAlive() == false)
{
life = RAND(1.0f, 2.0f); // Сделаем её снова живой
pos = CPos(0,0,0);
return;
}

// Применим скорость
pos.x += vel.x * dt;
pos.y += vel.y * dt;
pos.z += vel.z * dt;

// Применим гравитацию
pos.y += kParticleGravity * dt;

life -= dt; // Уменьшим оставшееся время жизни

// Применим вращение на «angle» в секунду, если он > 0
/*
if(angle != 0.0f)
texture.setRotation(texture.getRotAngle() + (angle * dt));
*/

this->angleNow = this->angleNow + (angle * dt);
}

void CParticle::render()
{
// Если частица не «жива», рендерить нечего
if(isAlive() == false)
return;

// Не могу рендерить с отсутствующей текстурой
if(this->textureID < 0)
return;

// Эта функция OpenGL делает Z-буфер доступным только для чтения. Это значит, что
// OpenGL использует текущие значения Z-буфера для определения, должна ли быть частица
// отрендерена или нет, НО, если частица не отрисовывается, никакие значения Z-буфера
// не будут установлены.
// Почему мы так делаем: Мы хотим, частицы были прозрачными, и не перекрывали друг
// друга. Теперь мы может рендерить их с той же Z-глубиной, и они будут прозрачными,
// но в то же время они не будут отрисовыватся в чем-то, что меньше их Z-глубины.   
glDepthMask(false);

// Установим цвет и текстуры
glColor4ub(GET_R(color), GET_G(color), GET_B(color), GET_A(color));

// Применим вращение на текстуру и забиндим её:
glMatrixMode(GL_TEXTURE);
glLoadIdentity();

glTranslatef(0.5f, 0.5f, 0.0f); // Центр текстуры — центр новой системы координат
glRotatef(this->angleNow, 0.0f, 0.0f, 1.0f); // Вращаем по оси Z

glTranslatef(0.5f, 0.5f, 0.0f); // Перемещаем назад

glBindTexture(GL_TEXTURE_2D, this->textureID); // Привязываем текстуру
glMatrixMode(GL_MODELVIEW);

glPushMatrix();

// Переместим частицы в указанные координаты
glTranslatef(pos.x, pos.y, pos.z);

float halfSize = size * 0.5f;

// Отрисуем частицы
glBegin(GL_QUADS);
glTexCoord2f(0.0f, 1.0f);
glVertex3f(halfSize, halfSize, 0.0f); // Верхняя левая вершина

glTexCoord2f(0.0f, 0.0f);
glVertex3f(halfSize, halfSize, 0.0f); // Нижняя левая вершина

glTexCoord2f(1.0f, 0.0f);
glVertex3f(halfSize, halfSize, 0.0f); // Нижняя правая вершина

glTexCoord2f(1.0f, 1.0f);
glVertex3f(halfSize, halfSize, 0.0f); // Верхняя правая вершина
glEnd();

glPopMatrix();

glDepthMask(true); // Переключим Z-буфер в нормальное состояние read-write
}

 

Ну и наконец используем все вышенакоденное %)

Переходим к файлу main.cpp:

// В инклуды добавим новый файл:
#include «particle.h»// После обьявим макс. количество частиц и добавим новые переменные:
#define MAX_PARTICLES 256CParticle gParticles[MAX_PARTICLES];      // Массив частиц
float gRGB[3] = {0};                      // Мировой RGB-цвет// Добавим прототипы новых функций:
void UpdateBkgrndColor();       // Изменяет мировой цвет// Теперь добавим две новых функции:
///////////////////////////////////////////////////////////////
//
//          Функция изменяет бекграунд-цвет сцены
//
///////////////////////////////////////////////////////////////
void UpdateBkgrndColor()
{
// Насколько увеличивать R,G и B компоненты цвета
static float inc[3] = { 0.005f, 0.005f, 0.005f };

int which = rand()%3; // Рандомно выберем компонент для увеличения

for(int i = 0; i < 3; ++i)
{
if(i == which)
{
gRGB[i] += inc[i];

// Удерживаем значения 0<>1
if(gRGB[i] > 1.0f)
{
gRGB[i] = 1.0f;
inc[i] = inc[i];
}
else if(gRGB[i] < 0.0f)
{
gRGB[i] = 0.0f;
inc[i] = inc[i];
}
}
}
}

///////////////////////////////////////////////////////////////////////////////
//
//  Функция возвращает true, если число FPS менее 60 (или указанного),
//      и false если более.
//
///////////////////////////////////////////////////////////////////////////////
bool LockFrameRate(int frame_rate = 60)
{
static float lastTime = 0.0f;

// Текущее время в секундах:
float currentTime = GetTickCount() * 0.001f;

// Получаем прошедшее с предыдущего кадра время. Если прошло достаточно, возвращаем true.
if((currentTime lastTime) > (1.0f / frame_rate))
{
// Сбросим последнее время
lastTime = currentTime;
return true;
}

return false;
}

////////////////////////////////////////////////////////////////////////////////////////
//
//  Инициализируем всё необходимое в функции Init():
//
////////////////////////////////////////////////////////////////////////////////////////
void Init(HWND hWnd)
{
g_hWnd = hWnd;
GetClientRect(g_hWnd, &g_rRect);
InitializeOpenGL(g_rRect.right, g_rRect.bottom);

// Инициализируем класс текстур
Texture = new CTexture();
// Загружаем нужную нам текстуру:
Texture->LoadTexture(IL_BMP, «particle.bmp», &textures[0]);

// Инициализируем генератор случайных чисел
srand(GetTickCount());

// Инициализируем все частицы:
for(int i=0; i<MAX_PARTICLES; i++)
{
// Если не получилось инициализировать какую-нибуть частицу — выходим
if(!gParticles[i].init(CPos(0,0,0), CVector(RAND(0.25f, 0.25f),
RAND(0.5f,1.5f), 0.0f),
RAND(1.0f, 2.0f), 0.75f, 30.0f,
ARGB(255, 215, 115, 40), textures[0].texID))
{
MessageBox(NULL, «Не могу инициализировать частицы.», «ERROR»,
MB_OK | MB_ICONERROR);
exit(1);
}
}

// Включим прозрачность для того, чтобы текстуры частиц были прозрачными:
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_COLOR, GL_ONE);
}

// И, наконец, главная функция программы:

///////////////////////////////////////////////////////////////
//
//          Функция вызывается каждый кадр и рендерит сцену
//
///////////////////////////////////////////////////////////////

void RenderScene()
{

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
gluLookAt(0,0,5, 0,0,0, 0,1,0);

// Переменные для нашего таймера
static float beginTime = GetTickCount() * .001f;
static float endTime = beginTime;
static float dt = 0.0f; // Delta Time — время, прошедшее с прошлого кадра

if(LockFrameRate())
{
beginTime = GetTickCount() * 0.001f;    // Получаем время начала кадра
dt = beginTime endTime;   // Получаем dt

// Установим бекграунд-цвет сцены в «gRGB»:
glClearColor(gRGB[0], gRGB[1], gRGB[2], 1.0f);

// Обработаем и отрисуем все частицы:
for(int i = 0; i < MAX_PARTICLES; ++i)
{
gParticles[i].render();
gParticles[i].process(dt);
}

UpdateBkgrndColor();

// Получим время, прошедшее с конца кадра
endTime = GetTickCount() * 0.001f;
}

SwapBuffers(g_hDC);
}

 

Надеюсь, этот первый опыт с частицами вам пригодится.

Исходные коды к уроку