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

Визуальные эффекты — ключевой компонент современной игры. Использование видео в качестве динамической текстуры для шейдеров открывает двери к созданию потрясающих атмосферных эффектов: от мерцающих порталов до искажённых голограмм. В этой статье мы разберём пример загрузки видео и его передачи в GLSL-шейдер для обработки в реальном времени, что позволит вам оживить игровую графику без использования сложных спрайт-листов или тяжёлой анимации.

Версия 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.video('trainSequence', 'assets/video/train512x256.mp4', true);
        this.load.image('noise', 'assets/tests/rgba-noise-medium.png');
        this.load.glsl('hue-shift', 'assets/shaders/hue-shift.frag');
    }

    create ()
    {
        const video = this.add.video(400, 300, 'trainSequence').setVisible(false);

        //  We're using this texture as a shader input.
        video.saveTexture('train');

        video.play(true);

        video.once('textureready', () => {

            this.add.shader({
                name: 'Hue Shift',
                fragmentKey: 'hue-shift',
                initialUniforms: {
                    resolution: [ this.scale.width, this.scale.height ],
                    iChannel0: 0,
                    iChannel1: 1
                },
                setupUniforms: (setUniform, drawingContext) => {
                    setUniform('time', this.game.loop.getDuration());
                }
            }, 400, 300, 800, 600, [ 'train', 'noise' ]);

        });
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#000000',
    parent: 'phaser-example',
    scene: Example
};

let game = new Phaser.Game(config);

Загрузка ресурсов: видео, изображение и шейдер

Первый шаг — подготовка всех необходимых ресурсов в методе preload. Для работы эффекта нам потребуется видеофайл, текстурный шум (для дополнительных визуальных искажений) и сам шейдерный код.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.video('trainSequence', 'assets/video/train512x256.mp4', true);
    this.load.image('noise', 'assets/tests/rgba-noise-medium.png');
    this.load.glsl('hue-shift', 'assets/shaders/hue-shift.frag');
}

Обратите внимание на параметры загрузки видео. Третий аргумент true в методе this.load.video означает, что видео будет загружено как поток (blob), что необходимо для его последующего использования в WebGL-контексте в качестве текстуры. Шейдер загружается с помощью метода this.load.glsl, который ожидает путь к файлу с кодом на GLSL.

Создание видео и подготовка текстуры

В методе create мы создаём игровой объект Video. Однако нам не нужно, чтобы само видео отрисовывалось на сцене как обычный спрайт — мы будем использовать его кадры как данные для шейдера.

const video = this.add.video(400, 300, 'trainSequence').setVisible(false);
video.saveTexture('train');
video.play(true);

Метод setVisible(false) скрывает стандартный видеоплеер. Ключевой вызов — video.saveTexture('train'). Этот метод создаёт из видеопотока специальную текстуру WebGL с именем 'train'. Теперь к кадрам видео можно обращаться в шейдере как к обычной текстуре. Вызов video.play(true) запускает воспроизведение видео в цикле (true указывает на зацикливание).

Ожидание готовности и создание шейдера

Текстура из видео не доступна мгновенно. Phaser предоставляет событие textureready, которое срабатывает, когда видео загружено и готово к использованию в WebGL. Вся логика создания шейдера помещается в обработчик этого события.

video.once('textureready', () => {
    this.add.shader({
        name: 'Hue Shift',
        fragmentKey: 'hue-shift',
        initialUniforms: {
            resolution: [ this.scale.width, this.scale.height ],
            iChannel0: 0,
            iChannel1: 1
        },
        setupUniforms: (setUniform, drawingContext) => {
            setUniform('time', this.game.loop.getDuration());
        }
    }, 400, 300, 800, 600, [ 'train', 'noise' ]);
});

Конфигурация шейдера передаётся в метод this.add.shader. Параметр fragmentKey ссылается на загруженный шейдерный код. Массив [ 'train', 'noise' ] в конце — это самый важный момент: он передаёт имена текстур ('train' — видео, 'noise' — загруженное изображение) в шейдер, где они становятся доступны как iChannel0 и iChannel1 соответственно. Функция setupUniforms обновляет uniform-переменную time каждый кадр, используя this.game.loop.getDuration(), что позволяет создавать анимированные эффекты (например, сдвиг оттенка).

Настройка сцены и игры

Финальная часть — стандартная конфигурация игры Phaser. В этом примере важно, чтобы размеры шейдера (800x600) и его позиция (400, 300) соответствовали желаемой области отрисовки.

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#000000',
    parent: 'phaser-example',
    scene: Example
};
let game = new Phaser.Game(config);

Чёрный фон (backgroundColor: '#000000') хорошо контрастирует с видеоконтентом. Класс Example, содержащий логику сцены, передаётся в конфигурацию игры.

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

Вы научились превращать видеопоток в динамическую текстуру для шейдеров в Phaser 3. Это мощный приём для создания сложных визуальных эффектов, таких как магические барьеры, экраны мониторов в игре или искажённые воспоминания. Для экспериментов попробуйте

  1. использовать другой шейдер (например, для размытия или обнаружения краёв)
  2. передать в шейдер несколько видео одновременно, смешивая их
  3. управлять uniform-переменными (например, интенсивностью эффекта) в реальном времени с помощью клавиатуры или мыши