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

Динамическое редактирование текстур — мощный инструмент, который позволяет создавать визуальные эффекты прямо во время работы игры. Вместо того чтобы загружать десятки предварительно обработанных изображений, вы можете модифицировать их программно, экономя ресурсы и добавляя гибкости. В этой статье мы разберем пример, где на основе обычного спрайта создается его красный силуэт, используя Canvas API и работу с пиксельными данными.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    originalTexture;
    context;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('dude', 'assets/sprites/phaser-dude.png');
    }

    create ()
    {
        this.originalTexture = this.textures.get('dude').getSourceImage();

        const newTexture = this.textures.createCanvas('dude_new', this.originalTexture.width, this.originalTexture.height);

        this.context = newTexture.getSourceImage().getContext('2d');

        this.context.drawImage(this.originalTexture, 0, 0);

        const dude = this.add.image(100, 100, 'dude');
        const dude2 = this.add.image(200, 100, 'dude_new');

        this.createSilhouette();
    }

    createSilhouette()
    {
        const pixels = this.context.getImageData(0, 0, this.originalTexture.width, this.originalTexture.height);

        for (let i = 0; i < pixels.data.length / 4; i++)
        {
            this.processPixel(pixels.data, i * 4, 0.1);
        }
        this.context.putImageData(pixels, 0, 0);
    }

    processPixel(data, index)
    {
        data[index] = 255;
        data[index + 1] = 0;
        data[index + 2] = 0;
    }
}

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

const game = new Phaser.Game(config);

Создание текстуры-холста

Перед модификацией необходимо создать новую текстуру, которая будет выступать в роли холста для рисования. Phaser предоставляет для этого метод this.textures.createCanvas.

const newTexture = this.textures.createCanvas('dude_new', this.originalTexture.width, this.originalTexture.height);

Этот код создает новую текстуру с ключом 'dude_new' и размерами, идентичными исходному спрайту 'dude'. Важно отметить, что эта текстура сразу становится доступной в кэше текстур игры.

Для последующего рисования нам нужен контекст 2D этого холста, который получается стандартным для Canvas способом.

this.context = newTexture.getSourceImage().getContext('2d');

Получив контекст, мы копируем на этот холст исходное изображение. Теперь у нас есть две идентичные текстуры в памяти: оригинальная и её копия на холсте, готовую к манипуляциям.

Извлечение и итерация по пикселям

Сердце примера — функция createSilhouette. Вся магия происходит на уровне пикселей. Для работы с ними используется ImageData — объект, содержащий одномерный массив данных каждого пикселя.

const pixels = this.context.getImageData(0, 0, this.originalTexture.width, this.originalTexture.height);

Метод getImageData возвращает объект, свойство data которого — это Uint8ClampedArray. В этом массиве данные о цвете каждого пикселя хранятся последовательно в формате RGBA: красный (R), зеленый (G), синий (B) и альфа-канал (A, прозрачность). Таким образом, информация об одном пикселе занимает 4 элемента массива.

Цикл перебирает все пиксели изображения. Условие i < pixels.data.length / 4 гарантирует, что мы обработаем каждый пиксель ровно один раз. Для каждого пикселя вызывается функция processPixel, которой передается массив данных и индекс начала данных этого пикселя (i * 4).

Логика обработки одного пикселя

Функция processPixel — это место, где определяется конечный визуальный эффект. В нашем примере она реализует простейшую логику для создания красного силуэта.

processPixel(data, index)
{
    data[index] = 255;   // Красный канал (R)
    data[index + 1] = 0; // Зеленый канал (G)
    data[index + 2] = 0; // Синий канал (B)
}

Код игнорирует исходные цвета пикселя. Для каждого из них он устанавливает значение красного канала на максимум (255), а зеленый и синий — в ноль. Альфа-канал (data[index + 3]) не трогается, поэтому исходная прозрачность (форма спрайта) сохраняется. В результате все непрозрачные пиксели становятся чисто красными.

После обработки всего массива пикселей, измененные данные необходимо вернуть на холст.

this.context.putImageData(pixels, 0, 0);

Вызов putImageData применяет модифицированный массив pixels к контексту, и текстура 'dude_new' мгновенно обновляется на экране.

Практическое применение и демонстрация

В методе create демонстрируется итог работы: на сцену добавляются два спрайта — оригинальный и модифицированный.

const dude = this.add.image(100, 100, 'dude');
const dude2 = this.add.image(200, 100, 'dude_new');

Вызывая this.createSilhouette(), мы запускаем процесс перекрашивания второго спрайта. Такой подход открывает двери для множества эффектов: от затемнения или обесцвечивания спрайтов при получении урона до динамического создания теней, подсветки или цветовой фильтрации целых слоев игры, не загружая дополнительные ассеты.

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

Работа с ImageData и Canvas-текстурами — это низкоуровневый, но крайне эффективный способ контролировать графику в Phaser. Вы научились создавать текстуру-холст, читать и модифицировать её пиксели. Для экспериментов попробуйте изменить processPixel: реализуйте сепию, инверсию цвета или сделайте силуэт не красным, а полупрозрачным черным для эффекта тени. Можно также изменять не все пиксели, а только те, чья яркость или альфа-канал превышает определенный порог.