Современные игры не были бы так хороши без системы частиц.
Пыль от колёс, дым из выхлопной трубы, звёздочки в магических играх, огонь, брызги — вот это все реализовано с помощью системы частиц.
Хорошая система частиц потребляет мало памяти, быстро работает и имеет многочисленные настройки.
В 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
, все остальные вычисления производятся в шейдере.