Пользовательские атрибуты вершин

В основе рендеринга в PixiJS лежит работа с буферами вершин и шейдерами WebGL. Любая геометрия представляется набором вершин, каждая из которых содержит набор атрибутов — числовых данных, передаваемых в вершинный шейдер. Классическая конфигурация включает:

  • позицию (aVertexPosition);
  • координаты текстуры (aTextureCoord);
  • цвет (aColor);
  • дополнительные параметры трансформации.

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

В PixiJS работа с атрибутами осуществляется через классы:

  • PIXI.Geometry
  • PIXI.Buffer
  • PIXI.Shader
  • PIXI.Mesh

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


Структура атрибутов в Geometry

PIXI.Geometry хранит:

  • буферы (PIXI.Buffer);
  • описание атрибутов (имя, размерность, тип данных, смещение и шаг).

Добавление пользовательского атрибута выполняется методом addAttribute.

Пример создания геометрии с дополнительным атрибутом смещения:

const geometry = new PIXI.Geometry()
    .addAttribute(
        'aVertexPosition',
        [
            0, 0,
            100, 0,
            100, 100,
            0, 100
        ],
        2
    )
    .addAttribute(
        'aOffset',
        [
            0, 0,
            10, 0,
            10, 10,
            0, 10
        ],
        2
    )
    .addIndex([0, 1, 2, 0, 2, 3]);

Параметры addAttribute

addAttribute(name, buffer, size, normalized, type, stride, start)
  • name — имя атрибута, совпадающее с именем в шейдере;
  • buffer — массив данных или экземпляр PIXI.Buffer;
  • size — количество компонентов на вершину (1–4);
  • normalized — требуется ли нормализация;
  • type — тип данных (PIXI.TYPES.FLOAT по умолчанию);
  • stride — шаг в байтах;
  • start — смещение в байтах.

Связь атрибутов с шейдером

Пользовательский атрибут должен быть объявлен в вершинном шейдере:

attribute vec2 aVertexPosition;
attribute vec2 aOffset;

uniform mat3 translationMatrix;
uniform mat3 projectionMatrix;

void main(void)
{
    vec3 position = projectionMatrix * translationMatrix *
                    vec3(aVertexPosition + aOffset, 1.0);

    gl_Position = vec4(position.xy, 0.0, 1.0);
}

Ключевые требования:

  1. Имя атрибута в Geometry и в GLSL должно совпадать.
  2. Размерность (vec2, vec3, float) должна соответствовать параметру size.
  3. Тип данных должен соответствовать type буфера.

Использование PIXI.Buffer

Для динамически изменяемых данных создаётся отдельный PIXI.Buffer:

const offsetBuffer = new PIXI.Buffer(new Float32Array([
    0, 0,
    10, 0,
    10, 10,
    0, 10
]), true);

geometry.addAttribute('aOffset', offsetBuffer, 2);

Второй параметр true означает, что буфер динамический (static = false), что позволяет обновлять данные:

offsetBuffer.data[0] = 5;
offsetBuffer.update();

Когда использовать динамический буфер

  • вершинная анимация;
  • физические симуляции;
  • интерактивные эффекты;
  • потоковые данные.

Интерливинг атрибутов

Для повышения производительности данные могут храниться в одном буфере с использованием шага (stride) и смещения (start).

Пример структуры:

[x, y, offsetX, offsetY,
 x, y, offsetX, offsetY,
 ...]

Создание:

const buffer = new PIXI.Buffer(new Float32Array([
    0, 0, 0, 0,
    100, 0, 10, 0,
    100, 100, 10, 10,
    0, 100, 0, 10
]));

geometry
    .addAttribute('aVertexPosition', buffer, 2, false, PIXI.TYPES.FLOAT, 16, 0)
    .addAttribute('aOffset', buffer, 2, false, PIXI.TYPES.FLOAT, 16, 8);

Здесь:

  • stride = 16 байт (4 float × 4 байта);
  • start = 0 для позиции;
  • start = 8 для смещения.

Интерливинг снижает количество привязок буферов и повышает эффективность передачи данных в GPU.


Типы данных атрибутов

PixiJS поддерживает различные типы:

  • PIXI.TYPES.FLOAT
  • PIXI.TYPES.UNSIGNED_BYTE
  • PIXI.TYPES.UNSIGNED_SHORT
  • PIXI.TYPES.INT
  • PIXI.TYPES.SHORT

Пример использования цветового атрибута:

geometry.addAttribute(
    'aCustomColor',
    new Uint8Array([
        255, 0, 0, 255,
        0, 255, 0, 255,
        0, 0, 255, 255,
        255, 255, 0, 255
    ]),
    4,
    true,
    PIXI.TYPES.UNSIGNED_BYTE
);

Параметр normalized = true преобразует значения из диапазона [0, 255] в [0.0, 1.0].

В шейдере:

attribute vec4 aCustomColor;
varying vec4 vColor;

void main(void)
{
    vColor = aCustomColor;
}

Передача данных во фрагментный шейдер

Пользовательские атрибуты доступны только в вершинном шейдере. Для передачи во фрагментный используется varying:

varying float vWeight;
attribute float aWeight;

void main(void)
{
    vWeight = aWeight;
}

Во фрагментном:

varying float vWeight;

void main(void)
{
    gl_FragColor = vec4(vec3(vWeight), 1.0);
}

Производительность и ограничения

Ограничение количества атрибутов

WebGL имеет лимит на количество одновременно используемых атрибутов. Его можно получить:

renderer.gl.getParameter(renderer.gl.MAX_VERTEX_ATTRIBS);

Обычно доступно 8–16 атрибутов.

Рекомендации

  • использовать минимально необходимое число компонентов;
  • применять интерливинг при больших объёмах данных;
  • избегать частых пересозданий буферов;
  • обновлять только изменённые данные.

Пример: волновая деформация поверхности

Создание пользовательского атрибута амплитуды:

const amplitudeBuffer = new PIXI.Buffer(new Float32Array([
    0.0,
    0.5,
    1.0,
    0.5
]), true);

geometry.addAttribute('aAmplitude', amplitudeBuffer, 1);

Вершинный шейдер:

attribute float aAmplitude;
uniform float uTime;

void main(void)
{
    vec2 position = aVertexPosition;
    position.y += sin(uTime + aAmplitude * 10.0) * 10.0;

    vec3 transformed = projectionMatrix *
                       translationMatrix *
                       vec3(position, 1.0);

    gl_Position = vec4(transformed.xy, 0.0, 1.0);
}

Обновление времени:

app.ticker.add((delta) => {
    shader.uniforms.uTime += 0.1 * delta;
});

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


Работа с Mesh

После настройки геометрии и шейдера создаётся PIXI.Mesh:

const mesh = new PIXI.Mesh(geometry, shader);
app.stage.addChild(mesh);

Mesh связывает:

  • данные вершин;
  • шейдерную программу;
  • текстуры и uniform-переменные.

Отладка пользовательских атрибутов

Типичные проблемы:

  1. Несоответствие размерности (size и vecX).
  2. Ошибочный stride или start.
  3. Неправильный тип данных.
  4. Отсутствие вызова buffer.update().

Для диагностики полезно:

  • временно выводить атрибут как цвет;
  • проверять массивы данных в консоли;
  • упрощать шейдер до минимальной версии.

Расширенные сценарии применения

Пользовательские атрибуты используются для:

  • GPU-частиц (индекс, скорость, возраст);
  • морфинга геометрии;
  • скиннинга (веса костей);
  • процедурной генерации;
  • кастомных эффектов освещения;
  • масок и градиентов;
  • хранения идентификаторов для условной логики в шейдере.

Гибкость механизма атрибутов позволяет полностью переопределить поведение стандартных примитивов и реализовать сложные графические эффекты без изменения ядра PixiJS.