Playing with Phaser and Three.js: preload glTF 3d models, render them with Three then animate them with Phaser

Talking about 3D, HTML5, Javascript, Phaser and TypeScript.

Things are getting interesting using Phaser with Three.js: earlier this week I published the latest step of the tutorial about building a HTML5 Stairs game using Phaser and Three, but if you want to render something more complicated than base geometries, today I am showing you how to import glTF files.

From Wikipedia: glTF is a standard file format for three-dimensional scenes and models. A glTF file uses one of two possible file extensions: .gltf (JSON/ASCII) or .glb (binary). Both .gltf and .glb files may reference external binary and texture resources. Alternatively, both formats may be self-contained by directly embedding binary data buffers (as base64-encoded strings in .gltf files or as raw byte arrays in .glb files).

We’ll see the case about .gltf extension with base64-encoded strings.

First, we need a glTF model, which I downloaded from Kay Lousberg’s itch page.

The model has been imported into a Phaser HTML5 canvas which runs the particle example you can find at this link.

Look how I mixed everything:

Look: there’s a 3D rotating knight and a 2D particle effect in the background, everything on the same canvas.

Let’s have a look at the commented source code: we have one HTML file, one CSS file and three TypeScript files.

index.html

The web page which hosts the game, to be run inside thegame element.

<!DOCTYPE html>
<html>
    <head>
        <meta name="viewport" content="initial-scale=1, maximum-scale=1">
        <link rel="stylesheet" href="style.css">
        <script src="main.js"></script> 
    </head>
    <body>   
        <div id = "thegame"></div>
    </body>
</html>

style.css

The cascading style sheets of the main web page.

* {
    padding : 0;
    margin : 0;
}

body {
    background-color: #000000;    
}

canvas {
    touch-action : none;
    -ms-touch-action : none;
}

main.ts

This is where the game is created, with all Phaser related options.

// MAIN GAME FILE

// modules to import
import Phaser from 'phaser';
import { PreloadAssets } from './preloadAssets';
import { PlayGame } from './playGame';

// object to initialize the Scale Manager
const scaleObject : Phaser.Types.Core.ScaleConfig = {
    mode : Phaser.Scale.FIT,
    autoCenter : Phaser.Scale.CENTER_BOTH,
    parent : 'thegame',
    width : 500,
    height : 500
}

// game configuration object
const configObject : Phaser.Types.Core.GameConfig = {
    type : Phaser.AUTO,
    backgroundColor : 0x000000,
    scale : scaleObject,
    scene : [PreloadAssets, PlayGame]
}

// the game itself
new Phaser.Game(configObject);

preloadAssets.ts

Here we preload all assets to be used in the game, such as the sprite atlas with the particles and the knight object.

// CLASS TO PRELOAD ASSETS
 
// this class extends Scene class
export class PreloadAssets extends Phaser.Scene {
 
    // constructor    
    constructor() {
        super({
            key : 'PreloadAssets'
        });
    }
 
    // method to be execute during class preloading
    preload(): void {

        // this is how we preload a generic text, in our case the GLTF JSON of the knight model
        this.load.text('knight', 'assets/objects/character_knight.gltf');

        // this is how we preload a sprite atlas
        this.load.atlas('flares', 'assets/sprites/flares.png', 'assets/sprites/flares.json');
    }
 
    // method to be called once the instance has been created
    create(): void {
 
        // call PlayGame class
        this.scene.start('PlayGame');
    }
}

playGame.ts

Main game file, all game logic and Three integration is stored here.

// THE GAME ITSELF

import * as THREE from 'three';
import {GLTFLoader} from 'THREE/examples/jsm/loaders/GLTFLoader.js'

// this class extends Scene class
export class PlayGame extends Phaser.Scene {

    // the GLTF model is a Three group
    knight : THREE.Group;

    constructor() {
        super({
            key: 'PlayGame'
        });
    }

    // method to be executed when the scene has been created
    create() : void {  
        
        // this is an adaptation of the demo found at
        // https://phaser.io/examples/v3/view/game-objects/particle-emitter/random-emit-zone
        
        var shape1 : Phaser.Geom.Circle = new Phaser.Geom.Circle(0, 0, 220);
        var shape2 : Phaser.Geom.Circle = new Phaser.Geom.Circle(0, 0, 240);

        var particles : Phaser.GameObjects.Particles.ParticleEmitterManager = this.add.particles('flares');

        particles.createEmitter({
            frame : 'red',
            x : 250,
            y : 250,
            lifespan : 2000,
            quantity : 4,
            scale : 0.2,
            alpha : {
                start : 1,
                end : 0
            },
            blendMode : 'ADD',
            emitZone : {
                type : 'random',
                source : shape1 as Phaser.Types.GameObjects.Particles.RandomZoneSource
            }
        });

        particles.createEmitter({
            frame : 'yellow',
            x : 250,
            y : 250,
            speed : 0,
            lifespan : 1000,
            quantity : 1,
            scale : {
                start : 0.4,
                end : 0
            },
            blendMode : 'ADD',
            emitZone : {
                type : 'edge',
                source : shape2,
                quantity : 48,
                yoyo : false
            }
        });

        // INTERESTING PART BEGINS HERE

        // create a new THREE scene
        const threeScene : THREE.Scene = new THREE.Scene();

        // initialize the Three group
        this.knight = new THREE.Group();

        // create the renderer
        const renderer : THREE.WebGLRenderer = new THREE.WebGLRenderer({
            canvas : this.sys.game.canvas,
            context : this.sys.game.context as WebGLRenderingContext,
            antialias : true
        });
        renderer.autoClear = false;
        renderer.shadowMap.enabled = true;
        renderer.shadowMap.type = THREE.PCFSoftShadowMap;

        // set up the GLTF loader, load JSON model, then add it to the scene
        const gltfLoader = new GLTFLoader();
        gltfLoader.parse(this.cache.text.get('knight'), '', (gltf) => {
            this.knight = gltf.scene;
            this.knight.position.set(0, 0, 0);
            this.knight.scale.set(20, 20, 20)
            threeScene.add(this.knight); 
        })

        // add a camera
        const camera : THREE.PerspectiveCamera = new THREE.PerspectiveCamera();
        camera.position.set(0, 17, 50);
        camera.lookAt(0, 17, 0);

        // add an ambient light
        const ambientLight : THREE.AmbientLight = new THREE.AmbientLight(0xffffff, 0.8);
        threeScene.add(ambientLight);
         
        // add a spotlight
        const spotLight : THREE.SpotLight = new THREE.SpotLight(0xffffff);
        spotLight.position.set(0, 0, 50);
        spotLight.target.position.set(0, 0, 0);
        threeScene.add(spotLight);
        threeScene.add(spotLight.target);

        // create an Extern Phaser game object
        const view : Phaser.GameObjects.Extern = this.add.extern();
        
        // custom renderer
        // next line is needed to avoid TypeScript errors
        // @ts-expect-error
        view.render = () => {
            renderer.state.reset();
            renderer.render(threeScene, camera);
        };        
    }

    update() : void {
        this.knight.rotateY(0.02)
    }
}

Loading a complex 3D model in Phaser using Three.js was easy, and this opens endless possibilities to game development. Let’s see how will this evolve, meanwhile download the source code.