Instanced rendering вручную

Instanced rendering — это техника отрисовки большого количества однотипных объектов с использованием одного набора геометрии и одного draw-вызова. В основе лежит возможность GPU применять разные атрибуты к каждой «инстанции» (экземпляру) геометрии без повторной передачи вершинных данных.

В WebGL 1 instancing реализуется через расширение ANGLE_instanced_arrays, в WebGL 2 — встроенными функциями:

  • gl.drawArraysInstanced
  • gl.drawElementsInstanced
  • gl.vertexAttribDivisor

В стандартном пайплайне PixiJS instancing применяется ограниченно. Для нестандартных задач (десятки тысяч однотипных спрайтов, частиц, тайлов) эффективнее реализовать instanced rendering вручную через низкоуровневый API PixiJS.


Архитектурная модель instanced rendering

Классическая схема без instancing:

для каждого объекта:
    bind geometry
    bind uniforms
    draw

С instancing:

bind geometry один раз
bind instance attributes
drawInstanced(количество_инстанций)

Разделение данных

Данные делятся на два типа:

  1. Per-vertex данные (общие для всех экземпляров)

    • позиция вершины
    • UV-координаты
    • базовая форма
  2. Per-instance данные

    • позиция экземпляра
    • масштаб
    • цвет
    • угол поворота
    • индекс текстуры
    • любые пользовательские параметры

GPU повторно использует вершинный буфер, меняя только атрибуты инстанций.


Низкоуровневый доступ в PixiJS

Для ручной реализации instancing используются:

  • PIXI.Geometry
  • PIXI.Buffer
  • PIXI.Shader
  • PIXI.Mesh
  • доступ к renderer.gl

Пример создания кастомной геометрии:

const geometry = new PIXI.Geometry()
    .addAttribute('aVertexPosition', vertices, 2)
    .addAttribute('aUV', uvs, 2)
    .addIndex(indices);

Для instancing потребуется дополнительный буфер с divisor.


Создание instanced-атрибутов

Instanced-атрибут отличается тем, что обновляется один раз на экземпляр, а не на вершину.

Шаг 1. Создание instance buffer

const instanceData = new Float32Array(maxInstances * 4); 
// например: x, y, scale, rotation

const instanceBuffer = new PIXI.Buffer(instanceData, false, false);

Шаг 2. Добавление атрибута с divisor

geometry.addAttribute(
    'aInstanceData',
    instanceBuffer,
    4,          // размер атрибута
    false,
    PIXI.TYPES.FLOAT,
    0,
    0,
    true        // <-- instanced
);

Последний параметр true включает режим instancing внутри PixiJS.

Внутри PixiJS это приводит к вызову vertexAttribDivisor.


Вершинный шейдер с instancing

Пример GLSL-шейдера:

precision mediump float;

attribute vec2 aVertexPosition;
attribute vec2 aUV;
attribute vec4 aInstanceData; // x, y, scale, rotation

uniform mat3 projectionMatrix;

varying vec2 vUV;

void main() {
    float x = aInstanceData.x;
    float y = aInstanceData.y;
    float scale = aInstanceData.z;
    float rotation = aInstanceData.w;

    vec2 scaled = aVertexPosition * scale;

    float cosR = cos(rotation);
    float sinR = sin(rotation);

    vec2 rotated = vec2(
        scaled.x * cosR - scaled.y * sinR,
        scaled.x * sinR + scaled.y * cosR
    );

    vec2 worldPosition = rotated + vec2(x, y);

    gl_Position = vec4((projectionMatrix * vec3(worldPosition, 1.0)).xy, 0.0, 1.0);

    vUV = aUV;
}

Ключевая особенность: геометрия остаётся неизменной, а трансформация берётся из атрибутов экземпляра.


Фрагментный шейдер

precision mediump float;

varying vec2 vUV;
uniform sampler2D uTexture;

void main() {
    gl_FragColor = texture2D(uTexture, vUV);
}

Принудительный instanced draw

По умолчанию PIXI.Mesh не вызывает drawInstanced. Необходимо вмешательство через renderer.geometry.

Переопределение draw

mesh._render = function(renderer) {
    const gl = renderer.gl;

    renderer.shader.bind(this.shader);
    renderer.geometry.bind(this.geometry);

    const ext = renderer.geometry.hasInstance
        ? renderer.gl.getExtension('ANGLE_instanced_arrays')
        : null;

    if (ext) {
        ext.drawElementsInstancedANGLE(
            gl.TRIANGLES,
            this.geometry.getIndex().data.length,
            gl.UNSIGNED_SHORT,
            0,
            instanceCount
        );
    }
};

В WebGL2:

gl.drawElementsInstanced(
    gl.TRIANGLES,
    indexCount,
    gl.UNSIGNED_SHORT,
    0,
    instanceCount
);

Динамическое обновление инстансов

При изменении параметров экземпляров необходимо:

instanceBuffer.update();

Если используется большой массив:

  • частичное обновление через buffer.update(data, offset)
  • double-buffering
  • persistent-mapped буферы (WebGL2)

Оптимизация памяти

Структура данных

Вместо:

[x, y, scale, rotation]

можно использовать:

mat3 или mat4

Преимущества:

  • меньше вычислений в шейдере
  • сложные трансформации
  • возможность skew

Недостатки:

  • больше памяти
  • больше bandwidth

Упаковка данных

Оптимизация через:

  • UNSIGNED_BYTE
  • HALF_FLOAT
  • нормализацию

Пример:

geometry.addAttribute(
    'aColor',
    colorBuffer,
    4,
    true,
    PIXI.TYPES.UNSIGNED_BYTE,
    0,
    0,
    true
);

Добавление цвета на экземпляр

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

attribute vec4 aColor;
varying vec4 vColor;

void main() {
    vColor = aColor;
}

Фрагментный:

varying vec4 vColor;

void main() {
    gl_FragColor = texture2D(uTexture, vUV) * vColor;
}

Работа с большим количеством объектов

При 50 000–200 000 экземпляров важно учитывать:

  • размер GPU-буфера
  • лимиты gl.MAX_VERTEX_ATTRIBS
  • ограничение на размер одного буфера
  • частоту обновления данных

Стратегии

  1. Разделение на чанки по 10–20k экземпляров
  2. Обновление только изменившихся диапазонов
  3. Использование texture buffers (WebGL2)
  4. Перенос части логики в шейдер

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

Instancing хорошо сочетается с текстурным атласом.

Дополнительный атрибут:

aFrame: vec4 (u0, v0, u1, v1)

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

attribute vec4 aFrame;

vUV = mix(aFrame.xy, aFrame.zw, aUV);

Это позволяет каждому экземпляру использовать свой фрейм, не меняя текстуру и не создавая новые draw-вызовы.


Ограничения instancing

  1. Невозможно использовать разные геометрии в одном вызове
  2. Ограничение по количеству атрибутов
  3. Высокая нагрузка на GPU при сложных шейдерах
  4. Затруднённая сортировка по глубине

При необходимости сортировки прозрачных объектов instancing может потребовать разделения на несколько проходов.


Сравнение с Sprite batching

Sprite batching Instanced rendering
Управляется Pixi автоматически Реализуется вручную
Работает только со спрайтами Работает с любой геометрией
Ограничен внутренней архитектурой Полный контроль
Меньше гибкости Максимальная гибкость

Instancing предпочтителен для:

  • систем частиц
  • тайловых карт
  • процедурной генерации
  • изометрических миров
  • рендеринга травы, листвы, декораций

WebGL1 и WebGL2 различия

WebGL1 WebGL2
Требуется ANGLE_instanced_arrays Встроенная поддержка
Нет UBO Есть UBO
Нет texture buffer Есть texture buffer
Ограниченная производительность Более эффективен

PixiJS автоматически выбирает контекст, но ручная реализация должна учитывать версию.


Расширенные техники

1. GPU-driven анимация

Передача:

  • времени
  • скорости
  • seed

Анимация выполняется полностью в шейдере.

2. Использование texture как instance data

В WebGL2 возможно хранить параметры экземпляров в текстуре и читать их через texelFetch.

Преимущества:

  • тысячи параметров
  • меньше атрибутов
  • более гибкая структура данных

Производительность

Instancing снижает:

  • количество draw calls
  • CPU overhead
  • нагрузку на драйвер

При 100 000 объектов:

  • без instancing — десятки миллисекунд CPU
  • с instancing — один draw call

GPU становится главным ограничением, а не CPU.


Типовой алгоритм реализации

  1. Создание базовой геометрии
  2. Создание instance buffer
  3. Добавление instanced-атрибутов
  4. Создание кастомного шейдера
  5. Переопределение draw-вызова
  6. Динамическое обновление буфера
  7. Оптимизация структуры данных

Практический кейс: рендер 50 000 спрайтов

  • одна геометрия квадрата
  • один шейдер
  • один texture atlas
  • один instanced draw

CPU обрабатывает только обновление массива параметров. GPU выполняет всю трансформацию и окрашивание.

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


Instanced rendering вручную в PixiJS открывает доступ к полноценному низкоуровневому контролю над WebGL, превращая библиотеку из 2D-фреймворка в гибкую GPU-платформу для высоконагруженной графики.