Система частиц с WebGL

Современные игры не были бы так хороши без системы частиц.

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

Хорошая система частиц потребляет мало памяти, быстро работает и имеет многочисленные настройки.

В 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

Дальше дело техники: отрисовать вершинный буфер с правильный шейдером и режимом смешивания.

Полные исходники на javascript
Файл ps.js

"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, все остальные вычисления производятся в шейдере.

Комментариев нет. Будьте первым!
Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Блог Евгения Жирнова