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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    image;
    lightNormalTools;
    specularNormalTools;

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

    create ()
    {
        // Base image.
        this.image = this.add.image(640, 360, 'gold');
        Phaser.Actions.FitToRegion(this.image, 1);

        const lightImage = this.add.image(640, 360, 'gold-n')
        .setBlendMode(Phaser.BlendModes.ADD)
        .setScale(this.image.scale)
        .enableFilters();
        this.lightNormalTools = lightImage.filters.internal.addNormalTools({
            outputRatio: true
        });
        const cm = lightImage.filters.internal.addColorMatrix();
        cm.colorMatrix.set([
            0.5, 0, 0, 0, 0,
            0, 0.4, 0, 0, 0,
            0, 0, 0.25, 0, 0,
            0, 0, 0, 1, 0,
        ]);

        const specularImage = this.add.image(640, 360, 'gold-n')
        .setBlendMode(Phaser.BlendModes.ADD)
        .setScale(this.image.scale)
        .enableFilters();
        this.specularNormalTools = specularImage.filters.internal.addNormalTools({
            outputRatio: true,
            ratioRadius: 0.1,
        });

        // Uncomment the following to isolate parts of the image:
        // this.image.setVisible(false);
        // lightImage.setVisible(false);
        // specularImage.setVisible(false);
    }

    update (time, delta)
    {
        this.lightNormalTools.ratioVector.set(
            Math.cos(time / 1000),
            Math.sin(time / 1000),
            0.3
        );
        this.specularNormalTools.ratioVector.set(
            Math.cos(time / 1000),
            Math.sin(time / 1000),
            0.3
        );
    }
}

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

const game = new Phaser.Game(config);

Подготовка сцены и загрузка текстур

В методе preload загружаются две текстуры: основное цветное изображение (gold) и его карта нормалей (gold-n). Карта нормалей — это специальное изображение, где цвет каждого пикселя кодирует направление нормали (перпендикуляра) к поверхности. Именно эта информация позволяет рассчитать, как свет должен падать на текстуру.

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

Создание базового изображения и слоев для эффектов

В create сначала создается основное изображение gold. Функция Phaser.Actions.FitToRegion масштабирует его под размер региона (в данном случае — 1 пиксель, что, вероятно, означает полное заполнение с сохранением пропорций). Затем создаются два дополнительных изображения, использующих карту нормалей gold-n. Ключевые моменты: - Оба слоя используют режим наложения Phaser.BlendModes.ADD для сложения цветов с основным изображением. - Для каждого слоя включаются фильтры методом .enableFilters(). - Фильтр Normal Tools добавляется через filters.internal.addNormalTools.

this.image = this.add.image(640, 360, 'gold');
Phaser.Actions.FitToRegion(this.image, 1);

const lightImage = this.add.image(640, 360, 'gold-n')
.setBlendMode(Phaser.BlendModes.ADD)
.setScale(this.image.scale)
.enableFilters();

Настройка фильтра Normal Tools и Color Matrix

Первому слою (lightImage) добавляется фильтр Normal Tools с параметром outputRatio: true. Это заставляет фильтр выводить не просто обработанную карту нормалей, а результат расчета соотношения (ratio) между направлением света и нормалями текстуры. Это и создает эффект освещения.

К этому же слою добавляется фильтр Color Matrix (addColorMatrix), который затемняет и окрашивает световой эффект, задавая интенсивность по каналам R, G, B. Это позволяет получить цветной свет (в примере — теплый, золотистый).

Второй слой (specularImage) также использует Normal Tools с outputRatio: true, но с дополнительным параметром ratioRadius: 0.1. Этот параметр влияет на «жесткость» или размытость эффекта, что часто используется для имитации бликов (specular highlights) — ярких отражений от глянцевых поверхностей.

this.lightNormalTools = lightImage.filters.internal.addNormalTools({
    outputRatio: true
});
const cm = lightImage.filters.internal.addColorMatrix();
cm.colorMatrix.set([
    0.5, 0, 0, 0, 0,
    0, 0.4, 0, 0, 0,
    0, 0, 0.25, 0, 0,
    0, 0, 0, 1, 0,
]);

this.specularNormalTools = specularImage.filters.internal.addNormalTools({
    outputRatio: true,
    ratioRadius: 0.1,
});

Анимация источника света в реальном времени

В методе update динамически изменяется свойство ratioVector у обоих экземпляров Normal Tools. Этот вектор задает направление, откуда падает свет, в 3D-пространстве (x, y, z). - Компоненты X и Y рассчитываются с помощью Math.cos и Math.sin от времени, что заставляет источник света плавно вращаться по кругу. - Компонента Z фиксирована на значении 0.3, задавая наклон света. Изменение этого вектора в реальном времени создает анимацию движущегося источника света и «бегающих» бликов по текстуре.

update (time, delta)
{
    this.lightNormalTools.ratioVector.set(
        Math.cos(time / 1000),
        Math.sin(time / 1000),
        0.3
    );
    this.specularNormalTools.ratioVector.set(
        Math.cos(time / 1000),
        Math.sin(time / 1000),
        0.3
    );
}

Отладка и изоляция эффектов

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

// Uncomment the following to isolate parts of the image:
// this.image.setVisible(false);
// lightImage.setVisible(false);
// specularImage.setVisible(false);

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

Фильтр Normal Tools с опцией outputRatio — это эффективный способ добавить динамическое освещение и блики к 2D-текстурам в Phaser, используя карты нормалей. Для экспериментов попробуйте: изменить значения в Color Matrix для получения света другого цвета; варьировать параметр ratioRadius для создания эффектов от мягкого свечения до резких бликов; анимировать Z-компоненту ratioVector для имитации изменения высоты источника света или использовать несколько источников света с разными векторами.