О чем этот пример
Рендеринг видео в реальном времени и применение к нему сложных шейдерных эффектов — мощный приём для создания атмосферных кат-сцен, магических интерфейсов или динамического фона в играх. В этой статье мы разберём пример из официальной библиотеки Phaser, который показывает, как захватывать кадры видео в динамическую текстуру, а затем использовать эту текстуру как источник для многослойного шейдерного эффекта, создавая иллюзию огня, сквозь который проступают анимированные персонажи. Этот подход открывает двери для нестандартных визуальных решений без необходимости предварительно обрабатывать видео в редакторах.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
constructor ()
{
super();
this.rt;
this.gos = [];
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.video('skeletonSequence', 'assets/video/skeleton.webm', true);
this.load.audio('tune', 'assets/audio/mag-overkill.m4a');
this.load.glsl('Fire Buffer A', 'assets/shaders/fire-buffer-a.frag');
this.load.glsl('Fire Buffer B', 'assets/shaders/fire-buffer-b.frag');
this.load.glsl('Fire', 'assets/shaders/fire.frag');
this.load.image('graveyard', 'assets/tests/graveyard.png');
}
create ()
{
const text = this.add.text(400, 300, 'Click to start', { font: '24px Courier', fill: '#00ff00' }).setOrigin(0.5);
this.input.once('pointerdown', () => {
text.destroy();
this.buildScene();
this.sound.play('tune', { loop: true });
});
}
buildScene ()
{
// Here's the Dynamic Texture
this.rt = this.textures.addDynamicTexture('videoBuffer', 1024, 1024);
// The Fire Buffer A shader.
this.bufferA = this.add.shader({
name: 'Fire Buffer A',
fragmentKey: 'Fire Buffer A',
initialUniforms: {
iChannel0: 0,
iChannel1: 1
},
setupUniforms: (setUniform, drawingContext) => {
setUniform('time', this.game.loop.getDuration());
}
}, 0, 0, 1024, 1024).setRenderToTexture("FireBufferA");
// The Fire Buffer B shader.
this.bufferB = this.add.shader({
name: 'Fire Buffer B',
fragmentKey: 'Fire Buffer B',
initialUniforms: {
iChannel0: 0,
iChannel1: 1
}
}, 0, 0, 1024, 1024).setRenderToTexture("FireBufferB");
// The final shader. It will render to a texture called 'FireShader'
// and will use the two buffers as its input textures.
this.shader = this.add.shader({
name: 'Fire',
fragmentKey: 'Fire',
initialUniforms: {
iChannel0: 0,
iChannel1: 1
}
}, 0, 0, 1024, 1024).setRenderToTexture('FireShader');
// Hook the sampler2D uniforms up for the multi-pass:
this.bufferA.setTextures([ 'videoBuffer', 'FireBufferB' ]);
this.bufferB.setTextures([ 'FireShader', 'FireBufferA' ]);
this.shader.setTextures([ 'FireBufferA', 'FireBufferB' ]);
// This image just holds the output of the Shader so we can see it on-screen
this.add.image(400, 216, 'graveyard');
this.add.image(0, 0, 'FireShader').setOrigin(0);
this.vid = this.add.video(400, 350, 'skeletonSequence').setDepth(1);
this.vid.saveTexture('skeleton');
this.vid.play(true);
this.vid.once('textureready', () => {
for (let i = 0; i < 11; i++)
{
let skelly = this.add.image(80 * i, 250, 'skeleton').setScale(0.2);
// Blends the skeleton feet nicely into the background
skelly.setAlpha(1, 1, 0.2, 0.2);
}
for (let i = 0; i < 10; i++)
{
let skelly = this.add.image(100 * i, 380, 'skeleton').setScale(0.5);
// Blends the skeleton feet nicely into the background
skelly.setAlpha(1, 1, 0.2, 0.2);
}
});
}
update ()
{
if (this.rt)
{
this.rt.clear();
this.rt.draw(this.vid, 0, 0);
this.rt.render();
}
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#000000',
parent: 'phaser-example',
scene: Example
};
let game = new Phaser.Game(config);
Подготовка ресурсов и базовая сцена
Класс Example расширяет Phaser.Scene. В конструкторе инициализируются свойства для хранения ссылок на динамическую текстуру (rt) и массив игровых объектов (gos).
В методе preload загружаются все необходимые ресурсы: видеофайл с анимацией скелета, аудиодорожка, три GLSL-шейдера и фоновое изображение. Обратите внимание на параметр true при загрузке видео — он включает предварительную буферизацию.
this.load.video('skeletonSequence', 'assets/video/skeleton.webm', true);
Метод create создаёт текстовую подсказку и ждёт клика пользователя, чтобы начать основное действие, уничтожая текст и запуская метод buildScene вместе с фоновой музыкой. Это стандартный паттерн для отложенной инициализации тяжёлых ресурсов.
Создание конвейера шейдерных текстур
Сердце примера — метод buildScene. Первым делом создаётся динамическая текстура (DynamicTexture) размером 1024x1024 под именем 'videoBuffer'. Эта текстура будет использоваться как холст для отрисовки каждого кадра видео.
this.rt = this.textures.addDynamicTexture('videoBuffer', 1024, 1024);
Затем создаются три шейдерных объекта с помощью this.add.shader. Первые два, bufferA и bufferB, настроены на рендеринг в текстуры (setRenderToTexture) с именами "FireBufferA" и "FireBufferB". Это так называемые буферы — промежуточные текстуры, которые будут использоваться в многослойном (multi-pass) шейдерном эффекте. Третий шейдер, this.shader, рендерит финальное изображение в текстуру 'FireShader'.
Ключевой момент — связывание этих текстур. Метод setTextures назначает текстуры, которые будут привязаны к uniform-переменным типа sampler2D (в данном случае iChannel0 и iChannel1) внутри шейдеров.
this.bufferA.setTextures([ 'videoBuffer', 'FireBufferB' ]);
this.bufferB.setTextures([ 'FireShader', 'FireBufferA' ]);
this.shader.setTextures([ 'FireBufferA', 'FireBufferB' ]);
Таким образом, шейдеры образуют циклическую цепочку, где выход одного может быть входом для другого на следующем кадре, создавая сложную, эволюционирующую текстуру огня.
Интеграция видео и создание спрайтов
После настройки шейдерного конвейера на сцену добавляется фоновое изображение ('graveyard') и изображение, отображающее финальную текстуру шейдера ('FireShader').
Далее создаётся видео-игровой объект (this.add.video) и проигрывается в цикле. Важнейший метод saveTexture вызывается у видеообъекта. Он захватывает текущий кадр видео и сохраняет его как статичную текстуру в текстовый менеджер Phaser под ключом 'skeleton'.
this.vid.saveTexture('skeleton');
Событие 'textureready' генерируется, когда текстура успешно создана и готова к использованию. В его обработчике создаются множественные спрайты (this.add.image), которые используют эту сохранённую текстуру. Каждый спрайт — это отдельный экземпляр изображения скелета, которому можно независимо задавать позицию, масштаб и прозрачность.
let skelly = this.add.image(80 * i, 250, 'skeleton').setScale(0.2);
skelly.setAlpha(1, 1, 0.2, 0.2);
Метод setAlpha с четырьмя аргументами позволяет задать разную прозрачность для каждого угла спрайта (top-left, top-right, bottom-left, bottom-right). В примере это используется, чтобы "ноги" скелетов (нижние углы) плавно сливались с фоном.
Живое обновление динамической текстуры
Чтобы шейдерный огонь реагировал на видео в реальном времени, каждый кадр видео должен передаваться в шейдерный конвейер. Это происходит в методе update.
if (this.rt)
{
this.rt.clear();
this.rt.draw(this.vid, 0, 0);
this.rt.render();
}
1. `this.rt.clear()`: Очищает динамическую текстуру `'videoBuffer'`.
2. `this.rt.draw(this.vid, 0, 0)`: Рисует текущий кадр видео (`this.vid`) в координатах (0, 0) этой текстуры.
3. `this.rt.render()`: Применяет все отложенные операции рисования на текстуре, делая изменения видимыми для шейдеров, которые используют `'videoBuffer'` как входную текстуру (`iChannel0` для `bufferA`).
Благодаря этому циклу, шейдеры получают обновляемый источник данных (видео), на основе которого генерируется эффект, создавая впечатление, что огонь "пожирает" изображение скелетов.
Что попробовать дальше
Пример демонстрирует мощную связку динамических текстур, видео и шейдеров в Phaser 3. Вы можете экспериментировать: замените видео на камеру пользователя для интерактивного эффекта, используйте другую цепочку шейдеров (например, для воды или искажения), или изменяйте параметры setAlpha и setBlendMode у спрайтов, чтобы добиться иного стиля интеграции с фоном. Главное — понимание принципа: видео рендерится в текстуру, которая становится сырьём для шейдерного процессора, результат которого можно как показывать на экране, так и использовать для создания множества спрайтов.
