File structure for Three.js as an Extern in Phaser 3.60

This is a quick post to follow up a discussion on the Phaser Discord.

The structure I have set up for the Match scene in Mr Football is as follows:

Match.js, where the Three.js init function sets up the Three scene as an Extern. Simplified code:

let dolly, tableau, director, renderer;

import { BaseScene } from './base.js'
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { MatchDisplay } from './matchdisplay.js'
import { MatchFunctions } from './matchfunctions.js'

export class Match extends BaseScene {
    constructor () {
        super({
            key: 'match'
        })
    }

init (data) {
    super.init();
    this.matchdata = data;
    this.objects = {};
    this.vertices = {};
    this.grip;
    this.func;
    }

create () {
    // declare Phaser variables here

    this.init3d();

    this.vertices.push( new THREE.Vector3( 0+1, 7.5+1, 0+1 ));

    // declare Phaser game objects, text, sprites etc here as children of this.objects
    // declare paths for Three objects as children of this.vertices

    const disp = new MatchDisplay(this.objects, this.vertices, tableau);
    this.func = new MatchFunctions(this.objects, disp, this.matchdata);

    // declare further Phaser game objects using this.func to access functions in the MatchFunctions scene, e.g. this.func.loadPlayer()
    }

init3d ()
    {
    dolly = new THREE.PerspectiveCamera(90, this.sys.canvas.width / this.sys.canvas.height, 1, 2000);

    dolly.position.x = 20;
    dolly.position.y = 350;
    dolly.position.z = 200;

    tableau = new THREE.Scene();

    dolly.lookAt(0, 0, 0);
    
    // import 3D objects or draw shapes in Three here

    // draw AmbientLight and maybe Spotlight in Three here

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

    this.grip = new OrbitControls(dolly, renderer.domElement);
    //this.grip.minPolarAngle = 0;
    //this.grip.maxPolarAngle = Math.PI/2-0.05;
    //this.grip.maxZoom = 0.05;
    this.grip.update();

    //  Create the Phaser Extern, tells Phaser to hand-off rendering to ThreeJS
    director = this.add.extern();
    renderer.setPixelRatio(1);
    dolly.aspect = this.sys.game.config.width/this.sys.game.config.height;
    dolly.updateProjectionMatrix();
    renderer.setSize(this.sys.canvas.width, this.sys.canvas.height);
    renderer.shadowMap.enabled = true;
    this.sys.game.scale.refresh();
    //renderer.autoClear = false;
    
    const gl = this.renderer.gl;
    //  The Extern render function
    director.render = () => {

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

        renderer.render(tableau, dolly);

        this.grip.update();

        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
        };
    
    //console.log('director', tableau, dolly, renderer);
    }
}

A lot of this was adapted from other examples you can find on the Web with a bit of googling, of course, and I have retained some of their comments. Most of the examples have you declaring “scene” as the Three.js variable, but I like to use tableau to differentiate it from the Phaser scene – YMMV, of course.

The initial line of vertices , which is what I label the paths that objects travel, is specified not to contain zeroes to prevent an arcane glitch – don’t go down that rabbit hole.

You are probably going to want to import OrbitControls.js even if you don’t allow the camera to wander from user input, as it makes moving the camera yourself a lot easier.

The line gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false); was suggested to me by @photonstorm as a solution for the tendency of Three to flip textures upside down. This is a problem that doesn’t really come through on the traditional examples, but it comes to the fore in Phaser 3.60 because not only does Three want to flip its own textures, it also flips all your font textures as well! This line prevents that – I don’t pretend to understand it but it works, happy days.

The other two files:

import { BaseScene } from './base.js'
import * as THREE from 'three';

export class MatchDisplay {
    constructor(objects, vertices, tableau) {
    this.objects = objects;
    this.vertices = vertices;
    this.tableau = tableau;
    }

    // Phaser object update functions here referencing this.objects and this.vertices

    // Three object update functions here referencing this.tableau
}

It should be noted that you don’t reference the match functions from the match display scene – they are daisy chained that way so that the main Match scene can invoke functions from the MatchFunctions scene and the MatchFunctions scene can invoke functions from the MatchDisplay scene.

import { BaseScene } from './base.js'
// note you do not need to import Three here

export class MatchFunctions {
    constructor(objects, disp, matchdata) {
    this.objects = objects;
    this.disp = disp;
    this.matchdata = matchdata;
    }
    
    // Phaser object update functions here referencing this.objects

    // Three object update functions here referencing this.tableau and also this.disp if needed
}

I hope this code would all work if you used it in your project, let me know if I’ve screwed anything up in the comments and I will amend it.