О чем этот пример
Визуальные эффекты — ключевой компонент современной игры. Использование видео в качестве динамической текстуры для шейдеров открывает двери к созданию потрясающих атмосферных эффектов: от мерцающих порталов до искажённых голограмм. В этой статье мы разберём пример загрузки видео и его передачи в 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. Это мощный приём для создания сложных визуальных эффектов, таких как магические барьеры, экраны мониторов в игре или искажённые воспоминания. Для экспериментов попробуйте
- использовать другой шейдер (например, для размытия или обнаружения краёв)
- передать в шейдер несколько видео одновременно, смешивая их
- управлять uniform-переменными (например, интенсивностью эффекта) в реальном времени с помощью клавиатуры или мыши
