Класс Shader и программа на GLSL

Графический движок PixiJS построен поверх WebGL, а значит, рендеринг объектов в конечном итоге сводится к выполнению программ на GPU. Эти программы называются шейдерами и пишутся на языке GLSL (OpenGL Shading Language).

Класс Shader в PixiJS представляет собой оболочку над парой GLSL-программ — вершинным (vertex shader) и фрагментным (fragment shader) шейдерами — и обеспечивает их интеграцию в систему рендеринга.

Шейдер управляет:

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

Архитектура WebGL-шейдера

Любой шейдер в PixiJS состоит из двух частей:

1. Вершинный шейдер (Vertex Shader)

Обрабатывает каждую вершину примитива. Основные задачи:

  • преобразование координат;
  • передача атрибутов во фрагментный шейдер;
  • работа с матрицами трансформации.

Пример минимального вершинного шейдера:

precision mediump float;

attribute vec2 aVertexPosition;
uniform mat3 translationMatrix;
uniform mat3 projectionMatrix;

void main() {
    vec3 position = projectionMatrix * translationMatrix * vec3(aVertexPosition, 1.0);
    gl_Position = vec4(position.xy, 0.0, 1.0);
}

Ключевые элементы:

  • attribute — входные данные от буфера вершин;
  • uniform — глобальные переменные, передаваемые из JavaScript;
  • gl_Position — обязательная переменная, определяющая позицию вершины.

2. Фрагментный шейдер (Fragment Shader)

Обрабатывает каждый пиксель (фрагмент). Определяет цвет итогового изображения.

Минимальный пример:

precision mediump float;

uniform vec4 uColor;

void main() {
    gl_FragColor = uColor;
}

gl_FragColor — итоговый цвет фрагмента.


Класс Shader в PixiJS

В PixiJS класс PIXI.Shader объединяет:

  • GLSL-программу,
  • набор uniform-переменных,
  • описание атрибутов.

Базовое создание шейдера:

const vertexSrc = `
precision mediump float;

attribute vec2 aVertexPosition;
uniform mat3 translationMatrix;
uniform mat3 projectionMatrix;

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

const fragmentSrc = `
precision mediump float;

uniform vec4 uColor;

void main() {
    gl_FragColor = uColor;
}
`;

const shader = PIXI.Shader.from(vertexSrc, fragmentSrc, {
    uColor: [1.0, 0.0, 0.0, 1.0]
});

Метод Shader.from():

  • компилирует GLSL-код;
  • создаёт WebGLProgram;
  • автоматически связывает uniform-переменные;
  • возвращает готовый объект шейдера.

Uniform-переменные

Uniform — это параметры, одинаковые для всех вершин или фрагментов во время одного вызова отрисовки.

Поддерживаемые типы:

GLSL тип JS формат
float number
vec2 [x, y]
vec3 [x, y, z]
vec4 [x, y, z, w]
mat3 Float32Array(9)
sampler2D Texture

Пример динамического обновления uniform:

shader.uniforms.uColor = [Math.random(), 0.0, 0.0, 1.0];

PixiJS автоматически синхронизирует значения с GPU перед рендерингом.


Связь Shader с Geometry и Mesh

Шейдер не используется напрямую для спрайтов. Он применяется через Mesh:

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

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

Компоненты:

  • Geometry — описание вершин;
  • Shader — логика обработки;
  • Mesh — связующий объект.

Работа с текстурами в шейдере

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

Fragment shader:

precision mediump float;

varying vec2 vTextureCoord;
uniform sampler2D uSampler;

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

Передача текстуры:

const texture = PIXI.Texture.from('image.png');

const shader = PIXI.Shader.from(vertexSrc, fragmentSrc, {
    uSampler: texture
});

PixiJS автоматически связывает текстуру с нужным текстурным юнитом WebGL.


Varying-переменные

varying используется для передачи данных из vertex shader во fragment shader.

Пример:

// vertex
varying vec2 vTextureCoord;
attribute vec2 aTextureCoord;

void main() {
    vTextureCoord = aTextureCoord;
}
// fragment
varying vec2 vTextureCoord;

GPU автоматически интерполирует значения между вершинами.


Матрицы трансформации

PixiJS передаёт в шейдер стандартные uniform-переменные:

  • translationMatrix
  • projectionMatrix

Они обеспечивают:

  • локальные трансформации объекта;
  • перевод координат в clip-space;
  • корректную работу камеры и масштабирования.

Правильный порядок умножения матриц критичен:

projectionMatrix * translationMatrix * position

Использование Program

Shader внутри себя использует объект Program.

Можно создать программу отдельно:

const program = PIXI.Program.from(vertexSrc, fragmentSrc);
const shader = new PIXI.Shader(program, {
    uColor: [0.0, 1.0, 0.0, 1.0]
});

Это полезно при повторном использовании одной GLSL-программы с разными uniform-наборами.


Кастомные атрибуты

Шейдер может работать с дополнительными атрибутами:

geometry.addAttribute('aCustomValue', [
    0.0, 1.0, 0.5, 0.8
], 1);

В vertex shader:

attribute float aCustomValue;
varying float vCustomValue;

void main() {
    vCustomValue = aCustomValue;
}

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

При работе с шейдерами важно учитывать:

  • минимизацию числа uniform-обновлений;
  • повторное использование Program;
  • избегание лишних ветвлений (if) в fragment shader;
  • сокращение операций с текстурами;
  • контроль precision (mediump, highp).

Фрагментный шейдер выполняется для каждого пикселя, поэтому именно он чаще всего становится узким местом.


Shader и Filter

Класс Filter в PixiJS построен на основе Shader. Фильтр — это специализированный шейдер, применяемый к уже отрисованной текстуре.

Отличие:

  • Shader применяется к геометрии;
  • Filter применяется к рендер-текстуре.

Фильтр использует те же GLSL-принципы, но встроен в систему постобработки.


Жизненный цикл шейдера

  1. Компиляция GLSL.
  2. Линковка WebGLProgram.
  3. Кэширование в PixiJS.
  4. Привязка перед draw-call.
  5. Обновление uniform.
  6. Освобождение ресурсов при уничтожении.

Удаление:

shader.destroy();

Освобождает связанные WebGL-ресурсы.


Частые ошибки

1. Несоответствие имён атрибутов Имя в Geometry.addAttribute() должно совпадать с attribute в GLSL.

2. Несоответствие типов uniform Передача массива вместо числа или наоборот приводит к ошибке компиляции или undefined-поведению.

3. Отсутствие precision в fragment shader WebGL требует явного указания precision.

4. Неверная работа с матрицами Нарушение порядка умножения приводит к искажению координат.


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

Анимация через uniform

let time = 0;

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

Fragment shader:

uniform float uTime;

void main() {
    float brightness = abs(sin(uTime));
    gl_FragColor = vec4(brightness, brightness, brightness, 1.0);
}

Процедурная графика

Шейдер позволяет полностью отказаться от текстур:

vec2 uv = gl_FragCoord.xy / resolution;
float color = sin(uv.x * 10.0);
gl_FragColor = vec4(vec3(color), 1.0);

Искажение текстуры

vec2 distorted = vTextureCoord;
distorted.y += sin(distorted.x * 10.0 + uTime) * 0.02;
gl_FragColor = texture2D(uSampler, distorted);

Внутренний механизм PixiJS

PixiJS:

  • управляет WebGLState;
  • кэширует активные программы;
  • отслеживает текущий Shader;
  • минимизирует переключение программ;
  • синхронизирует uniform только при изменении.

Это снижает нагрузку на GPU и CPU при сложных сценах.


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