О чем этот пример
Плавные переходы между изображениями — ключевой элемент визуальной динамики в играх. В этом примере мы используем шейдеры в Phaser для создания интерактивной радиальной маски-перехода, управляемой движением курсора. Вы научитесь загружать и комбинировать 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.image('pic1', 'assets/pics/robot-ai.jpg');
this.load.image('pic2', 'assets/pics/baal-ai.jpg');
this.load.glsl('gradient-color', 'assets/shaders/gradients/gradient-color.glsl');
this.load.glsl('gradient-process', 'assets/shaders/gradients/gradient-process.glsl');
this.load.glsl('srgb', 'assets/shaders/gradients/srgb-color.glsl');
this.load.glsl('value-circle', 'assets/shaders/gradients/value-circle.glsl');
}
create ()
{
this.add.image(400, 300, 'pic1');
const sprite = this.add.image(400, 300, 'pic2');
// Establish a progress value for the shader.
let progress = 0;
const feather = 0.05;
const getSource = (key) => this.cache.shader.get(key).glsl;
const gradientShader = this.add.shader({
name: 'Gradient',
shaderAdditions: [
// This addition controls a circle gradient.
// It must be defined in the shader before the gradient function is called.
{
name: 'CIRCLE',
additions: { fragmentHeader: getSource('value-circle') }
},
// This addition defines standard gradient functionality.
{
name: 'STANDARD',
additions: {
fragmentHeader: [
getSource('srgb'),
getSource('gradient-color')
].join('\n'),
fragmentProcess: getSource('gradient-process')
}
}
],
setupUniforms: (setUniform) => {
// Compute the position of the gradient from the progress value.
const amount = progress * feather;
const p = progress + amount;
// Constant feather:
const f = feather;
// Proportional feather (create a radial gradient):
// const f = p;
setUniform('center', [0.5, 0.5]);
setUniform('radius', p);
setUniform('feather', f);
setUniform('scale', [1, 800 / 600]); // Compensate for non-square aspect ratio.
setUniform('color1', [1, 1, 1, 1]); // Interior color.
setUniform('color2', [0, 0, 0, 0]); // Exterior color.
setUniform('steps', 0); // Smooth gradient.
setUniform('repeat', 1); // No tiling.
setUniform('offset', 0); // No offset.
}
}, 400, 300, 800, 600)
.setRenderToTexture('gradient');
sprite.enableFilters().filters.internal.addMask('gradient');
this.input.on('pointermove', pointer => {
// Get the distance in normalized space.
progress = Phaser.Math.Distance.Between(0.5, 0.5, pointer.worldX / 800, pointer.worldY / 600);
});
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
backgroundColor: '#0a0067',
parent: 'phaser-example',
scene: Example
};
let game = new Phaser.Game(config);
Загрузка ресурсов: картинки и шейдеры
В методе preload загружаются два изображения для демонстрации перехода и четыре GLSL-шейдерных файла. Ключевой момент — использование метода this.load.glsl. Он загружает текстовые файлы с кодом шейдеров и помещает их в кеш (this.cache.shader), откуда их можно будет извлечь позже как простой текст.
this.load.image('pic1', 'assets/pics/robot-ai.jpg');
this.load.image('pic2', 'assets/pics/baal-ai.jpg');
this.load.glsl('gradient-color', 'assets/shaders/gradients/gradient-color.glsl');
this.load.glsl('gradient-process', 'assets/shaders/gradients/gradient-process.glsl');
this.load.glsl('srgb', 'assets/shaders/gradients/srgb-color.glsl');
this.load.glsl('value-circle', 'assets/shaders/gradients/value-circle.glsl');
Создание шейдера и настройка униформов
В методе create сначала отрисовывается фоновое изображение (pic1), а поверх него — второе изображение (pic2), к которому будет применена маска. Основная работа происходит при создании объекта шейдера с помощью this.add.shader. Конфигурация шейдера состоит из двух частей: shaderAdditions и setupUniforms.
shaderAdditions позволяет собрать финальный код шейдера из нескольких загруженных кусков (additions). В данном случае определяются два блока: CIRCLE (задаёт форму градиента — круг) и STANDARD (определяет цветовую модель и функцию для расчёта градиента). Код из кеша извлекается с помощью вспомогательной функции getSource.
const getSource = (key) => this.cache.shader.get(key).glsl;
const gradientShader = this.add.shader({
name: 'Gradient',
shaderAdditions: [
{
name: 'CIRCLE',
additions: { fragmentHeader: getSource('value-circle') }
},
{
name: 'STANDARD',
additions: {
fragmentHeader: [
getSource('srgb'),
getSource('gradient-color')
].join('\n'),
fragmentProcess: getSource('gradient-process')
}
}
],
// ... setupUniforms будет рассмотрен далее
}, 400, 300, 800, 600)
.setRenderToTexture('gradient');
Функция setupUniforms вызывается каждый кадр для обновления параметров (uniforms) шейдера. Здесь рассчитывается радиус градиента на основе переменной progress и параметра размытия края feather. Униформы color1 и color2 задают цвета изнутри и снаружи круга (здесь белый и прозрачный черный). Униформа scale компенсирует не квадратное соотношение сторон экрана, чтобы круг не сплющился.
setupUniforms: (setUniform) => {
const amount = progress * feather;
const p = progress + amount;
const f = feather;
setUniform('center', [0.5, 0.5]);
setUniform('radius', p);
setUniform('feather', f);
setUniform('scale', [1, 800 / 600]);
setUniform('color1', [1, 1, 1, 1]);
setUniform('color2', [0, 0, 0, 0]);
setUniform('steps', 0);
setUniform('repeat', 1);
setUniform('offset', 0);
}
Метод .setRenderToTexture('gradient') указывает, что результат рендера этого шейдера должен быть сохранён в текстуру с именем gradient. Эта текстура будет использована как маска.
Применение шейдерной текстуры в качестве маски
Маски в Phaser позволяют скрывать части спрайта. В данном случае мы используем текстуру, сгенерированную шейдером, в качестве маски. Сначала нужно активировать систему фильтров для спрайта с помощью enableFilters(). Затем мы добавляем маску, обращаясь к внутреннему (internal) списку фильтров.
sprite.enableFilters().filters.internal.addMask('gradient');
Здесь 'gradient' — это имя текстуры, которое мы задали ранее в setRenderToTexture. Белые области текстуры-градиента будут показывать спрайт, черные — скрывать, а градиентные — создавать плавный переход.
Интерактивность: управление прогрессом курсором
Чтобы переход был динамическим, переменная progress обновляется при движении указателя мыши. Расстояние от центра экрана (в нормализованных координатах от 0 до 1) до позиции курсора становится новым значением прогресса. Это расстояние вычисляется с помощью Phaser.Math.Distance.Between.
this.input.on('pointermove', pointer => {
progress = Phaser.Math.Distance.Between(0.5, 0.5, pointer.worldX / 800, pointer.worldY / 600);
});
Поскольку setupUniforms вызывается каждый кадр, изменение progress мгновенно влияет на расчёт радиуса в шейдере, и маска плавно следует за курсором.
Что попробовать дальше
Вы создали высокопроизводительный интерактивный визуальный эффект, используя шейдеры как динамические маски в Phaser. Этот подход гораздо эффективнее, чем использование растровых масок или последовательностей спрайтов. Для экспериментов попробуйте: изменить форму градиента в шейдере value-circle.glsl на, например, квадратную волну; анимировать параметр feather для создания пульсирующего эффекта; использовать несколько масок с разными текстурами для сложных композиций; привязать прогресс не к курсору, а к времени или логике игры для автоматических переходов между локациями.
