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

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

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('stones', 'assets/normal-maps/stones.png');
        this.load.image('stones_normal', 'assets/normal-maps/stones_n.png');

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

    create ()
    {
        // Background.
        this.add.image(640, 360, 'stones').setScale(1.5);

        // Gradient texture.

        const getSource = (key) => this.cache.shader.get(key).glsl;

        const gradient = this.add.shader({
            name: 'Gradient',
            shaderAdditions: [
                // This addition controls a bilinear gradient.
                // It must be defined in the shader before the gradient function is called.
                {
                    name: 'BILINEAR',
                    additions: { fragmentHeader: getSource('value-bilinear') }
                },
                {
                    name: 'STANDARD',
                    additions: {
                        fragmentHeader: [
                            getSource('srgb'),
                            getSource('gradient-color')
                        ].join('\n'),
                        fragmentProcess: getSource('gradient-process')
                    }
                }
            ],
            setupUniforms: (setUniform) => {
                const period = 8;
                const progress = (this.game.loop.time / 1000) % period;
                const feather = 0.4;

                const position = progress - period / 2;

                setUniform('positionFrom', [0, 0]);
                setUniform('positionTo', [feather, 0.2 * feather]);
                setUniform('color1', [0, 0, 0, 0]);
                setUniform('color2', [0.8, 0.6, 0.2, 1]);
                setUniform('steps', 0);
                setUniform('repeat', 1);
                setUniform('offset', position);
            }
        }, 640, 360, 1024, 1024);

        gradient
            .setDisplaySize(1536, 1536)
            .setBlendMode(Phaser.BlendModes.ADD)
            .enableFilters();
        gradient.filters.internal.addDisplacement('stones_normal', 0.5, 0.5);
    }
}

const config = {
    type: Phaser.AUTO,
    width: 1280,
    height: 720,
    backgroundColor: '#2f3640',
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Загрузка ресурсов: картинки и шейдеры

В методе preload происходит подготовка всех необходимых ресурсов. В отличие от обычных изображений, шейдеры загружаются с помощью метода .load.glsl(). Это позволяет загрузить исходный код шейдера из внешнего файла и сохранить его в кеше для последующего использования.

this.load.image('stones', 'assets/normal-maps/stones.png');
this.load.image('stones_normal', 'assets/normal-maps/stones_n.png');

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

Создание композитного шейдера

В методе create создается основной визуальный объект — шейдер. Сначала определяется вспомогательная функция getSource, которая извлекает GLSL-код из кеша по ключу.

Ключевой момент — создание объекта шейдера с помощью this.add.shader(). Ему передается конфигурационный объект с параметрами shaderAdditions и setupUniforms. Параметр shaderAdditions позволяет собрать один сложный шейдер из нескольких загруженных модулей. В данном случае, модуль BILINEAR отвечает за билинейную интерполяцию значений, а STANDARD — за цветовую градиентную функцию и её обработку.

const getSource = (key) => this.cache.shader.get(key).glsl;

const gradient = this.add.shader({
    name: 'Gradient',
    shaderAdditions: [
        {
            name: 'BILINEAR',
            additions: { fragmentHeader: getSource('value-bilinear') }
        },
        {
            name: 'STANDARD',
            additions: {
                fragmentHeader: [
                    getSource('srgb'),
                    getSource('gradient-color')
                ].join('\n'),
                fragmentProcess: getSource('gradient-process')
            }
        }
    ],
    // ... setupUniforms
}, 640, 360, 1024, 1024);

Динамические uniform-переменные

Функция setupUniforms вызывается каждый кадр для обновления переменных (uniforms), передаваемых в шейдер. Это «мозг» анимации. Здесь рассчитывается progress на основе игрового времени, что создает циклическое движение градиента. Параметр feather управляет мягкостью перехода между цветами.

Переменные positionFrom и positionTo задают вектор направления градиента. color1 и color2 — это RGBA-значения начального и конечного цветов. offset — это ключевая анимируемая переменная, которая сдвигает градиент, создавая эффект движения.

setupUniforms: (setUniform) => {
    const period = 8;
    const progress = (this.game.loop.time / 1000) % period;
    
    const feather = 0.4;
    const position = progress - period / 2;

    setUniform('positionFrom', [0, 0]);
    setUniform('positionTo', [feather, 0.2 * feather]);
    setUniform('color1', [0, 0, 0, 0]); // Прозрачный черный
    setUniform('color2', [0.8, 0.6, 0.2, 1]); // Золотистый
    setUniform('steps', 0);
    setUniform('repeat', 1);
    setUniform('offset', position); // Анимируется со временем
}

Настройка отображения и постобработка

После создания объекта шейдера его необходимо настроить для интеграции в сцену. Метод .setDisplaySize() растягивает текстуру шейдера до нужных размеров. Установка режима смешивания Phaser.BlendModes.ADD приводит к тому, что цвета шейдера добавляются к цветам фона, создавая эффект яркого свечения.

Метод .enableFilters() активирует систему фильтров для этого объекта. Затем добавляется фильтр смещения (Displacement Filter), который использует карту нормалей stones_normal. Этот фильтр искажает текстуру градиента в соответствии с рельефом камней на фоне, создавая иллюзию объема и интеграции с окружением.

gradient
    .setDisplaySize(1536, 1536)
    .setBlendMode(Phaser.BlendModes.ADD)
    .enableFilters();
gradient.filters.internal.addDisplacement('stones_normal', 0.5, 0.5);

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

Этот пример раскрывает гибкость системы шейдеров Phaser. Вы можете экспериментировать, изменяя параметры в setupUniforms: попробуйте другие цвета, увеличьте feather для более размытого перехода или измените вектор positionTo для другого направления свечения. Замените карту нормалей на свою, чтобы градиент обтекал другие объекты. Сам подход модульной сборки шейдеров открывает путь к созданию библиотеки переиспользуемых визуальных эффектов для вашего проекта.