Написание собственного фильтра на GLSL

PixiJS предоставляет мощный механизм для работы с фильтрами через WebGL и GLSL. Создание собственного фильтра позволяет управлять визуальными эффектами на уровне пикселей, расширяя стандартный набор возможностей библиотеки.

Структура фильтра

Фильтр в PixiJS представляет собой объект класса PIXI.Filter, который включает два основных компонента:

  1. Вершинный шейдер (vertex shader) — отвечает за позицию вершин на экране.
  2. Фрагментный шейдер (fragment shader) — определяет цвет каждого пикселя.

Простейший фильтр можно создать с помощью:

const vertexSrc = `
    attribute vec2 aVertexPosition;
    attribute vec2 aTextureCoord;

    uniform mat3 projectionMatrix;

    varying vec2 vTextureCoord;

    void main(void) {
        vTextureCoord = aTextureCoord;
        gl_Position = vec4((projectionMatrix * vec3(aVertexPosition, 1.0)).xy, 0.0, 1.0);
    }
`;

const fragmentSrc = `
    varying vec2 vTextureCoord;
    uniform sampler2D uSampler;

    void main(void) {
        gl_FragColor = texture2D(uSampler, vTextureCoord);
    }
`;

const customFilter = new PIXI.Filter(vertexSrc, fragmentSrc);
sprite.filters = [customFilter];

Здесь vTextureCoord передаёт координаты текстуры от вершинного к фрагментному шейдеру, а uSampler обеспечивает доступ к текстуре спрайта.

Передача параметров в фильтр

Для динамического управления эффектом используются uniform-переменные. Они задаются при создании фильтра и могут изменяться в процессе работы приложения:

const fragmentSrc = `
    varying vec2 vTextureCoord;
    uniform sampler2D uSampler;
    uniform float intensity;

    void main(void) {
        vec4 color = texture2D(uSampler, vTextureCoord);
        color.rgb *= intensity;
        gl_FragColor = color;
    }
`;

const customFilter = new PIXI.Filter(null, fragmentSrc, { intensity: 1.0 });
customFilter.uniforms.intensity = 1.5;
sprite.filters = [customFilter];

Uniform-переменные позволяют анимировать фильтры или создавать интерактивные эффекты.

Использование текстур и нескольких uniform

Фильтры могут работать не только с базовой текстурой спрайта, но и с дополнительными текстурами для создания сложных эффектов:

const fragmentSrc = `
    varying vec2 vTextureCoord;
    uniform sampler2D uSampler;
    uniform sampler2D maskTexture;

    void main(void) {
        vec4 color = texture2D(uSampler, vTextureCoord);
        vec4 mask = texture2D(maskTexture, vTextureCoord);
        gl_FragColor = vec4(color.rgb * mask.a, color.a);
    }
`;

const maskTexture = PIXI.Texture.from('mask.png');
const customFilter = new PIXI.Filter(null, fragmentSrc, { maskTexture });
sprite.filters = [customFilter];

Использование дополнительных текстур позволяет реализовать маскирование, динамическое наложение эффектов и генерацию сложной графики.

Математические функции GLSL

GLSL поддерживает широкий набор функций для работы с цветом, координатами и векторами:

  • vec2, vec3, vec4 — векторы для хранения координат и цветов.
  • mix(a, b, t) — линейная интерполяция между a и b по коэффициенту t.
  • step(edge, x) — возвращает 0 или 1 в зависимости от сравнения.
  • smoothstep(edge0, edge1, x) — плавная интерполяция с градиентом.
  • length(v) — длина вектора v.
  • sin, cos, tan — тригонометрические функции для создания анимаций и волн.

Применение этих функций позволяет создавать эффекты размытия, свечения, искажения и динамические текстуры.

Пример: фильтр искажения

Создание эффекта волны или «текущей воды» реализуется с помощью смещения координат:

const fragmentSrc = `
    varying vec2 vTextureCoord;
    uniform sampler2D uSampler;
    uniform float time;

    void main(void) {
        vec2 uv = vTextureCoord;
        uv.y += 0.05 * sin(uv.x * 10.0 + time);
        gl_FragColor = texture2D(uSampler, uv);
    }
`;

const customFilter = new PIXI.Filter(null, fragmentSrc, { time: 0 });
app.ticker.add(delta => {
    customFilter.uniforms.time += delta * 0.1;
});
sprite.filters = [customFilter];

Здесь координата uv.y изменяется синусоидально во времени, создавая реалистичное движение.

Оптимизация фильтров

Для эффективной работы с большим количеством объектов:

  • Минимизировать количество uniform-переменных.
  • Использовать встроенные текстуры вместо генерации на лету, когда это возможно.
  • Избегать сложных циклов и ветвлений в фрагментном шейдере, так как они значительно замедляют GPU.
  • Переиспользовать один фильтр для нескольких спрайтов, если параметры одинаковы.

Интеграция с анимацией и событиями

Фильтры могут реагировать на пользовательский ввод или время, что позволяет создавать интерактивные эффекты:

window.addEventListener('mousemove', e => {
    customFilter.uniforms.offsetX = e.clientX / window.innerWidth;
});
app.ticker.add(delta => {
    customFilter.uniforms.time += delta * 0.05;
});

Такое управление открывает возможности для динамичных эффектов на игровом поле, интерфейсах и визуализациях данных.

Наследование фильтров

Можно создавать классы-наследники от PIXI.Filter, инкапсулируя шейдер и параметры:

class WaveFilter extends PIXI.Filter {
    constructor() {
        const fragmentSrc = `
            varying vec2 vTextureCoord;
            uniform sampler2D uSampler;
            uniform float time;

            void main(void) {
                vec2 uv = vTextureCoord;
                uv.y += 0.05 * sin(uv.x * 10.0 + time);
                gl_FragColor = texture2D(uSampler, uv);
            }
        `;
        super(null, fragmentSrc, { time: 0 });
    }

    update(delta) {
        this.uniforms.time += delta * 0.1;
    }
}

const waveFilter = new WaveFilter();
sprite.filters = [waveFilter];
app.ticker.add(delta => waveFilter.update(delta));

Наследование упрощает повторное использование фильтров и делает код более читаемым.

Создание фильтров на GLSL в PixiJS предоставляет полный контроль над визуальными эффектами, позволяя создавать уникальные графические решения и динамичные анимации.