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

Создание плавных и стильных переходов между сценами или объектами — частая задача в разработке игр. В этом примере мы реализуем эффект 'линейного стирания' (linear wipe), где одно изображение плавно сменяет другое по движению курсора мыши. Вместо использования готовых фильтров мы построим кастомную маску на основе GLSL-шейдера, что даст полный контроль над визуальным результатом и производительностью. Подход с динамической шейдерной маской особенно полезен для создания уникальных UI-эффектов, переходов между уровнями или интерактивных элементов, где стандартных средств Phaser может быть недостаточно. Вы научитесь комбинировать загруженные шейдерные модули и настраивать их параметры в реальном времени.

Версия 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-linear', 'assets/shaders/gradients/value-linear.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;
        let width = 0.02;

        const getSource = (key) => this.cache.shader.get(key).glsl;
        const gradientShader = this.add.shader({
            name: 'Gradient',
            shaderAdditions: [
                // This addition controls a linear gradient.
                // It must be defined in the shader before the gradient function is called.
                {
                    name: 'LINEAR',
                    additions: { fragmentHeader: getSource('value-linear') }
                },
                // 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.
                // We allow the gradient to move off-screen to the left and right
                // by the width of the gradient.
                const w = width / 2;
                const amount = Phaser.Math.Linear(-w, w, progress);
                const p = progress + amount;

                setUniform('positionFrom', [p - w, 0]);
                setUniform('positionTo', [p + w, 0]);
                setUniform('color1', [0, 0, 0, 0]);
                setUniform('color2', [1, 1, 1, 1]);
                setUniform('steps', 0);
                setUniform('repeat', 1);
            }
        }, 400, 300, 800, 600)
        .setRenderToTexture('gradient');

        sprite.enableFilters().filters.internal.addMask('gradient');

        this.input.on('pointermove', pointer => {

            progress = pointer.worldX / 800;

        });
    }
}

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(). Каждый файл содержит часть шейдерного кода: математические функции, обработку цвета и градиентов. Phaser помещает их в кеш шейдеров (this.cache.shader), откуда мы позже извлечем исходный текст.

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-linear', 'assets/shaders/gradients/value-linear.glsl');

Создание и конфигурация шейдера

В create() сначала отображаются два изображения друг на друга. Затем создается объект шейдера с помощью this.add.shader(). Конфигурация происходит через объект с несколькими ключевыми параметрами:

* shaderAdditions: Массив, определяющий, какие загруженные шейдерные модули и в каком порядке будут включены в итоговый шейдер. Модули 'LINEAR' и 'STANDARD' собираются воедино. * setupUniforms: Функция, которая вызывается каждый кадр для обновления uniform-переменных шейдера. Именно здесь рассчитывается динамика эффекта.

const gradientShader = this.add.shader({
    name: 'Gradient',
    shaderAdditions: [
        { name: 'LINEAR', additions: { fragmentHeader: getSource('value-linear') } },
        { name: 'STANDARD', additions: { fragmentHeader: [getSource('srgb'), getSource('gradient-color')].join('\n'), fragmentProcess: getSource('gradient-process') } }
    ],
    setupUniforms: (setUniform) => {
        const w = width / 2;
        const amount = Phaser.Math.Linear(-w, w, progress);
        const p = progress + amount;
        setUniform('positionFrom', [p - w, 0]);
        setUniform('positionTo', [p + w, 0]);
        setUniform('color1', [0, 0, 0, 0]);
        setUniform('color2', [1, 1, 1, 1]);
        setUniform('steps', 0);
        setUniform('repeat', 1);
    }
}, 400, 300, 800, 600).setRenderToTexture('gradient');

Шейдер рендерится в текстуру с именем 'gradient' методом .setRenderToTexture(). Эта текстура будет использоваться как маска.

Применение шейдерной текстуры в качестве маски

Теперь нужно применить созданную текстуру-градиент как маску к верхнему изображению (спрайту). Сначала для спрайта активируется система фильтров с помощью sprite.enableFilters(). Затем через filters.internal.addMask() добавляется маска, которая ссылается на текстуру 'gradient'.

sprite.enableFilters().filters.internal.addMask('gradient');

Маска работает по принципу альфа-канала: области, где текстура маски прозрачна (альфа = 0), скрывают соответствующие части спрайта, а непрозрачные области (альфа = 1) — оставляют видимыми. Наш шейдер как раз генерирует плавный черно-белый (прозрачно-непрозрачный) градиент.

Интерактивность: связываем градиент с курсором мыши

Чтобы эффект реагировал на движение мыши, мы подписываемся на событие 'pointermove'. В обработчике вычисляется значение progress (от 0 до 1) на основе горизонтальной позиции курсора (pointer.worldX) относительно ширины сцены (800 пикселей). Это значение progress используется в функции setupUniforms для расчета позиций positionFrom и positionTo, которые определяют, где находится градиентная зона стирания.

this.input.on('pointermove', pointer => {
    progress = pointer.worldX / 800;
});

Таким образом, перемещая мышь по горизонтали, игрок сам управляет границей перехода между двумя изображениями.

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

Вы построили полноценный интерактивный эффект перехода на основе шейдерной маски. Этот подход открывает путь к созданию сложных визуальных эффектов, не ограничиваясь встроенными возможностями движка. Для экспериментов попробуйте изменить переменную width, чтобы сделать границу перехода резкой или более размытой. Замените color1 и color2 на другие значения RGBA — так вы сможете создать не просто стирание, а, например, переход через цветную дымку. Самый сложный, но и самый гибкий путь — модификация загружаемых GLSL-файлов для создания радиальных, волнообразных или шумовых градиентов.