Частицы и системы частиц — распространённая вещь, которую вы можете увидеть в любой игре.
Обычно частицы создаются двумя способами. Первый — текстурированный квадрат, всегда обращенный к камере. Второй — использование точечных спрайтов, которые обычно являются массивами треугольников или точечными примитивами. В этом уроке мы будем использовать первый метод.
Итак, что составляет частицу? У вас может быть много разных переменных, но вот те, что мы будем использовать:
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:
#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 <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:
#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);
}
Надеюсь, этот первый опыт с частицами вам пригодится.