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

Хотите добавить трёхмерные эффекты или сложные шейдеры в свою 2D-игру на Phaser? Интеграция с Three.js открывает новые горизонты, позволяя совместить простоту Phaser с мощью WebGL-рендерера. В этой статье мы разберём практический пример: как встроить 3D-сцену Three.js в Phaser, управлять ей и даже применять к ней встроенные фильтры Phaser, такие как размытие. Этот подход полезен для создания гибридных игр, сложных UI-эффектов или визуализаций, где 2D и 3D должны работать вместе.

Версия 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.script('threejs', 'js/three145.js');
        this.load.image('logo', 'assets/sprites/phaser3-logo-small.png');
        this.load.image('bg', 'assets/skies/chrome.png');
    }

    create ()
    {
        this.add.image(640, 360, 'bg').setDisplaySize(1280, 720);

        this.init3D();

        this.add.image(0, 0, 'logo').setOrigin(0, 0);
    }

    //  This is all mostly ThreeJS code
    init3D ()
    {
        const camera = new THREE.PerspectiveCamera(70, 1280 / 720, 1, 10000);

        camera.position.z = 300;

        const scene = new THREE.Scene();

        const texture1 = new THREE.TextureLoader().load('assets/normal-maps/brick.jpg');
        const texture2 = new THREE.TextureLoader().load('assets/textures/gold.png');
        const texture3 = new THREE.TextureLoader().load('assets/textures/grass.png');

        const material1 = new THREE.MeshBasicMaterial({ map: texture1 });
        const material2 = new THREE.MeshBasicMaterial({ map: texture2 });
        const material3 = new THREE.MeshBasicMaterial({ map: texture3 });

        const geometry1 = new THREE.BoxGeometry(100, 100, 100);
        const mesh1 = new THREE.Mesh(geometry1, material1);

        const geometry2 = new THREE.SphereGeometry(64, 32, 16);
        const mesh2 = new THREE.Mesh(geometry2, material2);

        const geometry3 = new THREE.CylinderGeometry(35, 35, 80, 32);
        const mesh3 = new THREE.Mesh(geometry3, material3);

        mesh2.position.x = -200;
        mesh3.position.x = 200;

        scene.add(mesh1);
        scene.add(mesh2);
        scene.add(mesh3);

        //  Tell three to use the Phaser canvas
        //  Also: Notice we're using the WebGL1 Renderer here
        const renderer = new THREE.WebGL1Renderer({
            canvas: this.sys.game.canvas,
            context: this.sys.game.context,
            antialias: true,
        });

        //  Create the Phaser Extern, tells Phaser to hand-off rendering to ThreeJS
        const view = this.add.extern();

        renderer.setPixelRatio(1);
        renderer.setSize(1280, 720);

        //  You can skip this if threeJS is providing the _background_
        //  and all Phaser objects are on-top of it
        renderer.autoClear = false;

        //  The Extern render function
        view.render = (webGLRenderer, drawingContext, calcMatrix) => {

            //  This is essential to get ThreeJS to reset the GL state
            renderer.resetState();

            // Ensure the DrawingContext framebuffer is bound.
            webGLRenderer.glWrapper.updateBindingsFramebuffer({
                bindings: {
                    framebuffer: drawingContext.framebuffer
                }
            }, true);

            mesh1.rotation.x += 0.005;
            mesh1.rotation.y += 0.01;

            mesh2.rotation.x -= 0.005;
            mesh2.rotation.y += 0.02;

            mesh3.rotation.x += 0.03;
            mesh3.rotation.y += 0.02;

            renderer.render(scene, camera);

            //  Call it again, after rendering, if you get graphical corruption
            // renderer.resetState();

        };

        // Add extern to a framebuffer for Phaser filter effects.
        view.enableFilters();
        const blur = view.filters.external.addBlur();

        // Tween the blur strength.
        this.tweens.add({
            targets: blur,
            strength: 0,
            ease: 'Sine.easeInOut',
            duration: 3000,
            yoyo: true,
            repeat: -1
        });
    }
}

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

const game = new Phaser.Game(config);

Подготовка: загрузка Three.js и ассетов

Первый шаг — загрузить библиотеку Three.js как внешний скрипт. Phaser позволяет это сделать с помощью метода load.script(). Важно указать корректный базовый URL и путь к файлу Three.js. Параллельно загружаются обычные 2D-изображения для фона и логотипа.

this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.script('threejs', 'js/three145.js');
this.load.image('logo', 'assets/sprites/phaser3-logo-small.png');
this.load.image('bg', 'assets/skies/chrome.png');

Создание 3D-сцены Three.js

В методе init3D() создаётся стандартная сцена Three.js: камера, сцена, геометрия, материалы и меши (объекты). Код создаёт три простые фигуры с разными текстурами и располагает их в пространстве. Ключевой момент — рендерер Three.js настраивается на использование существующего canvas и WebGL-контекста Phaser.

const renderer = new THREE.WebGL1Renderer({
    canvas: this.sys.game.canvas,
    context: this.sys.game.context,
    antialias: true,
});

Настройка autoClear: false критична, если Three.js рисует фон, а Phaser-объекты должны отображаться поверх. Без этого Phaser может очищать буферы, удаляя 3D-сцену.

Мост между Phaser и Three.js: объект Extern

Phaser предоставляет специальный объект Extern для кастомного рендеринга. Создав его через this.add.extern(), мы можем переопределить его метод render(). Внутри этого метода происходит магия интеграции.

const view = this.add.extern();
view.render = (webGLRenderer, drawingContext, calcMatrix) => {
    renderer.resetState();
    webGLRenderer.glWrapper.updateBindingsFramebuffer({
        bindings: {
            framebuffer: drawingContext.framebuffer
        }
    }, true);
    // ... анимация вращения объектов ...
    renderer.render(scene, camera);
};

Вызов renderer.resetState() сбрасывает состояние WebGL, чтобы Three.js и Phaser не конфликтовали. Связывание framebuffer необходимо для корректного вывода 3D-изображения в текущий буфер Phaser. Анимация вращения объектов обновляется каждый кадр прямо в этом методе.

Применение фильтров Phaser к 3D-сцене

Одна из самых мощных возможностей — применение фильтров Phaser к рендеру Three.js. Объект Extern можно перевести в режим работы с фильтрами.

view.enableFilters();
const blur = view.filters.external.addBlur();

После этого к 3D-сцене можно добавлять стандартные фильтры, например, размытие. Его параметры можно анимировать с помощью системы Tween Phaser, создавая плавные эффекты.

this.tweens.add({
    targets: blur,
    strength: 0,
    ease: 'Sine.easeInOut',
    duration: 3000,
    yoyo: true,
    repeat: -1
});

Важные нюансы и отладка

Интеграция требует аккуратности. Если вы видите графические артефакты (например, чёрный экран или наложения), попробуйте вызвать renderer.resetState() дважды — до и после рендера Three.js. Убедитесь, что Three.js не пытается очистить буфер (autoClear: false), если фон отрисовывается Phaser. Также проверьте порядок отрисовки: в данном примере сначала добавляется фон Phaser, затем создаётся Extern, что гарантирует правильный z-order. Все пути к ассетам Three.js должны быть абсолютными или корректно относительными к базовому URL.

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

Интеграция Phaser и Three.js — мощный инструмент для разработчиков, желающих выйти за рамки 2D. Вы можете анимировать 3D-объекты, применять к ним постобработку Phaser и создавать гибридные сцены. Для экспериментов попробуйте: добавить взаимодействие (клики по 3D-объектам), использовать более сложные шейдеры Three.js или комбинировать несколько фильтров Phaser на одном Extern. Это открывает путь к уникальному визуальному стилю вашей игры.