О чем этот пример

Шейдеры — мощный инструмент для создания уникальных визуальных эффектов в реальном времени, таких как пикселизация, размытие или цветокоррекция. В этой статье мы разберем, как создать и настроить собственный шейдер в Phaser 3, используя встроенный API. Вы научитесь загружать текстуры, определять шейдерные программы и управлять их параметрами (uniforms) прямо из игровой логики.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('face', 'assets/pics/bw-face.png');
        this.load.image('metal', 'assets/textures/alien-metal.jpg');
        this.load.image('grass', 'assets/textures/grass.png');
        this.load.image('tiles', 'assets/textures/tiles.jpg');
        this.load.image('logo', 'assets/sprites/phaser3-logo-small.png');
    }

    create ()
    {
        const frag = `
        precision mediump float;

        uniform vec2 resolution;
        uniform float pixelSize;

        varying vec2 fragCoord;

        void main (void)
        {
            vec2 uv = fragCoord / resolution.xy;

            gl_FragColor = vec4(uv.xy, 1.0);
        }
        `;

        const base = new Phaser.Display.BaseShader(
            'pixelate',
            frag,
            null,
            {
                pixelSize: { type: '1f', value: 0.2 }
            }
        );

        const shader = this.add.shader(base, 400, 300, 800, 600, [ 'metal' ]);

        shader.setUniform('pixelSize.value', 0.2);

        console.log(shader);
    }
}

const game = new Phaser.Game({
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
});

Загрузка ресурсов и настройка сцены

Как и в любом проекте на Phaser, работа начинается с загрузки ресурсов в методе preload(). В этом примере мы загружаем несколько текстур, которые позже могут быть использованы как входные данные для шейдера.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('face', 'assets/pics/bw-face.png');
    this.load.image('metal', 'assets/textures/alien-metal.jpg');
    this.load.image('grass', 'assets/textures/grass.png');
    this.load.image('tiles', 'assets/textures/tiles.jpg');
    this.load.image('logo', 'assets/sprites/phaser3-logo-small.png');
}

Затем, в методе create(), мы создаем конфигурацию игры, указывая использование WebGL-рендерера. Это обязательное условие для работы с шейдерами.

Создание шейдерной программы

Ядро примера — фрагментный шейдер (Fragment Shader), написанный на GLSL. Этот шейдер просто отображает UV-координаты текстуры в виде цвета, создавая градиент.

const frag = `
precision mediump float;

uniform vec2 resolution;
uniform float pixelSize;

varying vec2 fragCoord;

void main (void)
{
    vec2 uv = fragCoord / resolution.xy;

    gl_FragColor = vec4(uv.xy, 1.0);
}
`;

Шейдер объявляет uniform-переменные resolution (разрешение области отрисовки) и pixelSize (параметр для эффекта, который мы могли бы использовать для пикселизации). Переменная fragCoord автоматически передается шейдеру и содержит координаты текущего пикселя.

Обертка шейдера в Phaser

Чтобы использовать шейдер в Phaser, его нужно обернуть в объект Phaser.Display.BaseShader. Этот объект связывает исходный код шейдера с его параметрами (uniforms).

const base = new Phaser.Display.BaseShader(
    'pixelate',
    frag,
    null,
    {
        pixelSize: { type: '1f', value: 0.2 }
    }
);

Конструктор принимает: имя шейдера, исходный код фрагментного шейдера, исходный код вершинного шейдера (в данном случае null, используется стандартный) и объект с описанием uniform-переменных. Для pixelSize указан тип '1f' (одно вещественное число) и начальное значение 0.2.

Добавление шейдера на сцену и управление параметрами

Созданный базовый шейдер добавляется на сцену как игровой объект с помощью метода this.add.shader().

const shader = this.add.shader(base, 400, 300, 800, 600, [ 'metal' ]);

Параметры метода: объект BaseShader, координаты X и Y центра шейдера, его ширина и высота, а также массив ключей загруженных текстур, которые будут переданы в шейдер. В данном случае передается текстура 'metal'.

После создания объекта шейдера мы можем динамически менять его uniform-переменные с помощью метода setUniform().

shader.setUniform('pixelSize.value', 0.2);

Этот вызов устанавливает значение uniform-переменной pixelSize в 0.2. Имя параметра передается как строка 'pixelSize.value'.

Что попробовать дальше

Вы создали и добавили на сцену кастомный шейдер, управляя его параметрами из кода игры. Для экспериментов попробуйте: изменить исходный код шейдера, чтобы реализовать настоящую пикселизацию; анимировать значение pixelSize с помощью tweens или в update()-цикле; передавать в шейдер другие текстуры из массива загруженных изображений.