Современные игры не были бы так хороши без системы частиц.
Пыль от колёс, дым из выхлопной трубы, звёздочки в магических играх, огонь, брызги — вот это все реализовано с помощью системы частиц.
Хорошая система частиц потребляет мало памяти, быстро работает и имеет многочисленные настройки.
В OpenGL обычно создаётся вершинный буфер, который содержит все частицы одного типа и отрисовывается такой буфер за один проход.
В нашей системе у частицы будут такие параметры:
— позиция
— прозрачность
— сила гравитации
— ускорение
— скорость
Идея такова: есть массив частиц, частица может быть включена или выключена. Для включенных частиц идёт обновление позиции, скорости и прозрачности. Каждая такая частица добавляется в вершинный буфер и все частицы рисуются за один проход.
Сначала демонстрация в цветовой гамме MS-DOS (поводите мышкой по черной области), затем пояснения и исходники на JavaScript:
Частица представлена у нас в виде javascript объекта:
var Particle = function() {
// Позиция частицы
this.x = 0;
this.y = 0;
// Скорость движения частицы по x, y
this.vx = 0;
this.vy = 0;
// Ускорение частицы по x, y
this.ax = 0;
this.ay = 0;
// Прозрачность частицы
this.alpha = 0;
// Изменение прозрачности в секунду
this.vAlpha = 0;
// Размер частицы
this.size = 0;
// Флаг, что частица активна
this.active = false;
};
Инициализация массива частиц:
// Инициализируем массив частиц
particles = new Array(numParticles);
for (var i = 0; i < numParticles; ++i) {
// Каждая частица изначально выключена
particles[i] = new Particle();
}
Чтобы частицы создавались непрерывно, введем переменную pps, она будет означать количество новых частиц в секунду. Для вычисления необходимого количества частиц, введем переменную realTime
типа float
. Она будет накапливать количество секунд для создания хотя бы одной частицы.
Формула для вычисления количества частиц:
newParticleCount = Math.floor(pps * realTime)
После вычисления количества, необходимо уменьшить таймер:
realTime -= newParticleCount / pps
Общий код создания частиц:
var newParticleCount = Math.floor(pps * realTime)
if (newParticleCount > 0) {
realTime -= newParticleCount / pps;
for (var i = 0; i < particles.length; ++i) {
if (newParticleCount <= 0) {
break;
}
var particle = particles[i];
if (!particle.active) {
add(particle);
newParticleCount--;
}
}
}
Код добавления частицы просто инициализирует частицу и делает ее активной
function add(particle) {
particle.x = someX;
particle.y = someY;
particle.vx = someVX;
particle.vy = someVY;
particle.ax = someAX;
particle.ay = someAY;
particle.alpha = someAlpha;
particle.vAlpha = someVAlpha;
particle.active = true;
}
Для придания реалистичности движения частицы используется ускорение и гравитация:
particle.vx += particle.ax * dt;
particle.vy += particle.ay * dt;
particle.vy -= gravity * dt;
Если частица полностью прозрачна или выходит за пределы экрана (слева, справа или снизу), то частица отключается и выпадает из обработки:
if (particle.alpha < 0 ||
particle.x < -1 || particle.x > 1 ||
particle.y < -1) {
particle.active = false;
}
После обновления позиции и прозрачности частицы, заполняем вершинный буфер, в котором на каждую частицу приходится два треугольника, шесть вершин и по три значения на вершину (x, y, alpha):
// Слева снизу
vertices[index++] = left;
vertices[index++] = bottom;
vertices[index++] = alpha;
// Справа снизу
vertices[index++] = right;
vertices[index++] = bottom;
vertices[index++] = alpha;
// Слева сверху
vertices[index++] = left;
vertices[index++] = top;
vertices[index++] = alpha;
// Справа снизу
vertices[index++] = right;
vertices[index++] = bottom;
vertices[index++] = alpha;
// Справа сверху
vertices[index++] = right;
vertices[index++] = top;
vertices[index++] = alpha;
// Слева сверху
vertices[index++] = left;
vertices[index++] = top;
vertices[index++] = alpha
Дальше дело техники: отрисовать вершинный буфер с правильный шейдером и режимом смешивания.
"use strict";
var vsSource = [
"attribute vec2 a_position;",
"attribute float a_alpha;",
"",
"varying float v_alpha;",
"",
"void main() {",
" gl_Position = vec4(a_position, 1.0, 1.0);",
" v_alpha = a_alpha;",
"}"
].join("\n");
var fsSource = [
"precision highp float;",
"",
"varying float v_alpha;",
"",
"void main() {",
" gl_FragColor = vec4(0.0, 0.905, 0.0, v_alpha);",
"}"
].join("\n");
var Particle = function() {
// Позиция частицы
this.x_ = 0;
this.y_ = 0;
// Скорость движения частицы по x, y
this.vx_ = 0;
this.vy_ = 0;
// Ускорение частицы по x, y
this.ax_ = 0;
this.ay_ = 0;
// Прозрачность частицы
this.alpha_ = 0;
// Изменение прозрачности в секунду
this.vAlpha_ = 0;
// Размер частицы
this.size_ = 0;
// Флаг, что частица активна
this.active_ = false;
};
Particle.prototype = {
constructor : Particle
}
var ParticleManager = function(numParticles, pps) {
// Всего float на частицу
this.FLOATS_PER_PARTICLE = 18;
// Количество новых частиц в секунду
this.pps_ = pps;
// Инициализируем массив частиц
this.particles_ = new Array(numParticles);
for (var i = 0; i < numParticles; ++i) {
this.particles_[i] = new Particle();
}
// Позиция эмиттера
this.emitterX_ = 0;
this.emitterY_ = 0;
// Начальная скорость частицы
this.velInit_ = 0.5;
// Разброс (+-) начальной скорости частицы
this.velDisp_ = 0.5;
// Начальное ускорение частицы
this.accInit_ = 0.0;
// Разброс (+-) начального ускорения частицы
this.accDisp_ = 0.025;
// Начальный размер частицы
this.sizeInit_ = 0.005;
// Разброс (+-) начального размера частицы
this.sizeDisp_ = 0.005;
// Сила гравитации, направлена вниз
this.gravity_ = 2.0;
// Массив вершин
this.vertices_ = new Float32Array(numParticles * this.FLOATS_PER_PARTICLE);
// Количество активных частиц
this.numActiveParticles_ = 0;
// Вершинный буфер
this.vbo_ = null;
// Шейдер, позиция в шейдере и прозрачность
this.shader_ = null;
this.positionId_ = -1;
this.alphaId_ = -1;
// Время для вычисления количества новых частиц
this.realTime_ = 0;
}
ParticleManager.prototype = {
constructor : ParticleManager,
// Активирование частицы
add : function(particle) {
if (particle.active_) {
return;
}
// Начальная позиция частицы совпадает с позицием эмиттера
particle.x_ = this.emitterX_;
particle.y_ = this.emitterY_;
// Вычисляем начальное ускорение частицы
particle.ax_ = this.accInit_ + (Math.random() - 0.5) * this.accDisp_;
particle.ay_ = this.accInit_ + (Math.random() - 0.5) * this.accDisp_;
// Направление движения частицы
var angle = Math.random() * Math.PI * 2.0;
var cosA = Math.cos(angle);
var sinA = Math.sin(angle)
// Скорость движения
var vel = (Math.random() - 0.5) * this.velDisp_;
// Скорость и направление движения частицы
particle.vx_ = (this.velInit_ + vel) * cosA;
particle.vy_ = (this.velInit_ + vel) * sinA;
// Размер частицы
particle.size_ = this.sizeInit_ + (Math.random() - 0.5) * this.sizeDisp_;
// Начальная прозрачность
particle.alpha_ = 1.0;
// Уменьшение прозрачности в секунду
particle.vAlpha_ = 0.25 + Math.random();
// Активируем частицу
particle.active_ = true;
},
update : function(dt) {
this.realTime_ += dt;
// Вычисляем количество новых частиц
var newParticleCount = Math.floor(this.pps_ * this.realTime_);
if (newParticleCount > 0) {
this.realTime_ -= newParticleCount / this.pps_;
for (var i = 0, count = this.particles_.length; i < count; ++i) {
if (newParticleCount <= 0) {
break;
}
var particle = this.particles_[i];
if (!particle.active_) {
this.add(particle);
newParticleCount--;
}
}
}
var numActiveParticles = 0;
var vertices = this.vertices_;
for (var i = 0, count = this.particles_.length; i < count; ++i) {
var particle = this.particles_[i];
if (!particle.active_) {
continue;
}
// Обновление скорости частицы
particle.vx_ += particle.ax_ * dt;
particle.vy_ += particle.ay_ * dt;
// Обновление позиции частицы
particle.x_ += particle.vx_ * dt;
particle.y_ += particle.vy_ * dt;
// Применение гравитации
particle.vy_ -= this.gravity_ * dt;
// Изменение прозрачности
particle.alpha_ -= particle.vAlpha_ * dt;
// Деактивация невидимой частицы
if (particle.alpha_ < 0) {
particle.active_ = false;
continue;
}
// Выключаем частицы за пределами экрана
if (particle.x_ < -1.0 || particle.x_ > 1.0) {
particle.active_ = false;
continue;
}
// Выключаем частицы за пределами экрана
if (particle.y_ < -1.0) {
particle.active_ = false;
continue;
}
var l = particle.x_ - particle.size_;
var t = particle.y_ + particle.size_;
var r = particle.x_ + particle.size_;
var b = particle.y_ - particle.size_;
var a = particle.alpha_;
var index = numActiveParticles * this.FLOATS_PER_PARTICLE;
vertices[index++] = l;
vertices[index++] = b;
vertices[index++] = a;
vertices[index++] = r;
vertices[index++] = b;
vertices[index++] = a;
vertices[index++] = l;
vertices[index++] = t;
vertices[index++] = a;
vertices[index++] = r;
vertices[index++] = b;
vertices[index++] = a;
vertices[index++] = r;
vertices[index++] = t;
vertices[index++] = a;
vertices[index++] = l;
vertices[index++] = t;
vertices[index++] = a;
numActiveParticles++;
}
this.numActiveParticles_ = numActiveParticles;
},
render : function(gl) {
if (0 == this.numActiveParticles_) {
return;
}
if (!this.shader_) {
this.initShader(gl);
}
if (!this.vbo_) {
this.vbo_ = gl.createBuffer();
}
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.enable(gl.BLEND);
gl.useProgram(this.shader_);
gl.bindBuffer(gl.ARRAY_BUFFER, this.vbo_);
gl.bufferData(gl.ARRAY_BUFFER, this.vertices_, gl.DYNAMIC_DRAW);
gl.vertexAttribPointer(
this.positionId_,
2,
gl.FLOAT,
false,
3 * Float32Array.BYTES_PER_ELEMENT,
0);
gl.enableVertexAttribArray(this.positionId_);
gl.vertexAttribPointer(
this.alphaId_,
1,
gl.FLOAT,
false,
3 * Float32Array.BYTES_PER_ELEMENT,
2 * Float32Array.BYTES_PER_ELEMENT);
gl.enableVertexAttribArray(this.alphaId_);
gl.drawArrays(gl.TRIANGLES, 0, this.numActiveParticles_ * 6);
gl.disableVertexAttribArray(this.alphaId_);
gl.disableVertexAttribArray(this.positionId_);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.useProgram(null);
},
initShader : function(gl) {
var vs = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vs, vsSource);
gl.compileShader(vs);
var fs = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fs, fsSource);
gl.compileShader(fs);
var shader = gl.createProgram();
gl.attachShader(shader, vs);
gl.attachShader(shader, fs);
gl.linkProgram(shader);
this.positionId_ = gl.getAttribLocation(shader, "a_position");
this.alphaId_ = gl.getAttribLocation(shader, "a_alpha");
this.shader_ = shader;
},
setPosition : function(x, y) {
this.emitterX_ = x;
this.emitterY_ = y;
}
}
Файл main.js
"use strict";
window.requestAnimFrame = (function() {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(callback, element) {
window.setTimeout(callback, 1000.0/30.0);
};
})();
var gl = null;
var lastTime = Date.now();
var particleManager = null;
function update(dt) {
particleManager.update(dt);
}
function drawFrame() {
gl.clearColor(0.0, 0.0, 0.0, 1.0);
gl.clear(gl.COLOR_BUFFER_BIT);
particleManager.render(gl);
}
function render() {
window.requestAnimFrame(render);
var time = Date.now();
var dt = (time - lastTime) * 0.001;
lastTime = time;
update(dt);
drawFrame();
}
function startRender() {
var canvas = document.getElementById("webgl-canvas");
gl = canvas.getContext("experimental-webgl");
gl.viewport(0, 0, canvas.width, canvas.height);
// Всего тысяча частиц и их рождается 100 штук в секунду
particleManager = new ParticleManager(1000, 100);
canvas.onmousemove = function(event) {
var bbox = canvas.getBoundingClientRect();
var x = (event.clientX - bbox.left) * (canvas.width / bbox.width);
var y = (event.clientY - bbox.top) * (canvas.height / bbox.height);
var u = x / (canvas.width) * 2 - 1.0;
var v = 1.0 - 2 * y / canvas.height;
particleManager.setPosition(u, v);
}
render();
}
Вы молодец, если дочитали до конца!
Поэтому я открою еще один способ создания частиц. Этот способ самый быстрый, но имеет ряд ограничений.
Основная идея заключается в том, что все данные частицы живут в статическом вершинном буфере и каждый аттрибут вершины зависит от переменной t
, которая принимает значения от нуля до единицы. Ваша задача подобрать формулу зависимости аттрибута от t
и при рисовании обновлять только t
, все остальные вычисления производятся в шейдере.