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

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

Версия 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('metal', 'assets/textures/alien-metal.jpg');
        this.load.image('grass', 'assets/textures/grass.png');
        this.load.image('tiles', 'assets/textures/tiles.jpg');
        this.load.image('logo', 'assets/sprites/phaser3-logo-small.png');
        this.load.glsl('tunnel', 'assets/shaders/tunnel.frag');
    }

    create ()
    {
        // Phaser 4: Use config object for shader
        const shader = this.add.shader({
            name: 'TunnelShader',
            fragmentKey: 'tunnel',
            initialUniforms: {
                alpha: 1,
                origin: 2,
                resolution: [ 800, 600 ],
                iChannel0: 0 // texture unit 0
            },
            setupUniforms: (setUniform, drawingContext) =>
            {
                setUniform('time', this.game.loop.getDuration());
            },
        }, 400, 300, 800, 600, [ 'metal' ]);

        shader.setInteractive();

        shader.on('pointerdown', () =>
        {
            const currentTexture = shader.textures[0].key;

            if (currentTexture === 'metal')
            {
                shader.setTextures([ 'grass' ]);
            }
            else if (currentTexture === 'grass')
            {
                shader.setTextures([ 'tiles' ]);
            }
            else
            {
                shader.setTextures([ 'metal' ]);
            }
        });

        this.add.image(400, 300, 'logo');
        
        this.add.text(10, 10, 'Click to change texture', { font: '16px Courier', fill: '#ffffff' }).setShadow(1, 1);
    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example
};

const game = new Phaser.Game(config);

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

Перед созданием шейдера необходимо загрузить все необходимые ресурсы. В методе preload мы используем this.load.image для загрузки текстур и this.load.glsl для загрузки шейдерного кода из файла.

this.load.image('metal', 'assets/textures/alien-metal.jpg');
this.load.image('grass', 'assets/textures/grass.png');
this.load.image('tiles', 'assets/textures/tiles.jpg');
this.load.image('logo', 'assets/sprites/phaser3-logo-small.png');
this.load.glsl('tunnel', 'assets/shaders/tunnel.frag');

Метод load.glsl загружает фрагментный шейдер (отвечающий за цвет пикселя) и сохраняет его под ключом 'tunnel'. Ключи для текстур ('metal', 'grass', 'tiles', 'logo') будут использоваться позже для их передачи в шейдер и создания спрайтов.

Создание и настройка шейдерного объекта

В Phaser 4 для создания шейдера используется метод this.add.shader, принимающий объект конфигурации и параметры позиционирования. Это более структурированный подход по сравнению с прямым вызовом конструктора.

const shader = this.add.shader({
    name: 'TunnelShader',
    fragmentKey: 'tunnel',
    initialUniforms: {
        alpha: 1,
        origin: 2,
        resolution: [ 800, 600 ],
        iChannel0: 0 // texture unit 0
    },
    setupUniforms: (setUniform, drawingContext) =>
    {
        setUniform('time', this.game.loop.getDuration());
    },
}, 400, 300, 800, 600, [ 'metal' ]);

Разберем ключевые параметры конфигурации: - fragmentKey: указывает на загруженный ранее шейдерный код ('tunnel'). - initialUniforms: задает начальные значения uniform-переменных шейдера. Например, iChannel0: 0 означает, что в первый тектурный юнит (слот) будет привязана текстура из первого элемента массива, переданного последним аргументом (в нашем случае 'metal'). - setupUniforms: функция-колбэк, вызываемая каждый кадр для обновления динамических uniform-переменных. Здесь мы обновляем time, передавая общее время работы игры, что необходимо для анимации эффекта туннеля.

Последний аргумент [ 'metal' ] — это массив ключей текстур, которые будут связаны с текстурными юнитами шейдера (iChannel0, iChannel1 и т.д.).

Интерактивность и динамическая смена текстур

Чтобы шейдер реагировал на ввод, его нужно сделать интерактивным с помощью setInteractive. После этого можно назначить обработчик события pointerdown.

shader.setInteractive();

shader.on('pointerdown', () =>
{
    const currentTexture = shader.textures[0].key;

    if (currentTexture === 'metal')
    {
        shader.setTextures([ 'grass' ]);
    }
    else if (currentTexture === 'grass')
    {
        shader.setTextures([ 'tiles' ]);
    }
    else
    {
        shader.setTextures([ 'metal' ]);
    }
});

Логика обработчика построена на цикличной ротации текстур. Свойство shader.textures содержит массив объектов текстур, привязанных к шейдеру. Мы проверяем ключ первой текстуры (shader.textures[0].key) и, в зависимости от его значения, меняем весь набор привязанных текстур с помощью метода setTextures. Этот метод принимает новый массив ключей и автоматически перепривязывает их к соответствующим текстурным юнитам шейдера (iChannel0, iChannel1 и т.д.). В данном примере используется только один текстурный слот.

Добавление интерфейсных элементов

Для завершения сцены добавим статичный логотип и текстовую подсказку. Они создаются после шейдера и отрисовываются поверх него.

this.add.image(400, 300, 'logo');
this.add.text(10, 10, 'Click to change texture', { font: '16px Courier', fill: '#ffffff' }).setShadow(1, 1);

Метод this.add.image создает спрайт с логотипом Phaser в центре экрана. this.add.text создает текстовый объект с инструкцией в левом верхнем углу. Стиль задается объектом конфигурации, а setShadow добавляет легкую тень для лучшей читаемости.

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

Динамическое управление текстурами шейдера открывает широкие возможности для игрового дизайна. Вы можете использовать этот механизм не только по клику, но и по таймеру, при столкновениях или изменении состояния игрока. Для экспериментов попробуйте: 1. Загрузить и переключать между несколькими шейдерами, используя setFragmentShader. 2. Передавать в шейдер больше одной текстуры (например, [ 'metal', 'grass' ]) и менять их независимо. 3. Связать смену текстуры с игровыми событиями, например, менять фон шейдера при получении урона или сборе бонуса. 4. Анимировать изменение uniform-переменных (например, alpha для плавного появления/исчезновения эффекта).