Dear friendy reader,

nice to see you around here. I publish these articles for free and it would be very kind of you to support my blog. You can start by disabling your AdBlocker. That would mean a lot to me and will help to finance this blog.

Best regards, J. Wiese

Phaser  3 - Realtime ligthing

Phaser 3 - Realtime ligthing

One completely new feature of Phaser 3 is the dynamic lighting layer. By adding light sources and marking sprites to receive lights it is possible to light them up. With Phaser 3.6.0 the default rendering path is forward lighting. The deferred lighting path is not ready yet. The shader exists but, somehow the pipeline file is missing(?).

To use the lighting layer, you must use the WEBGL-renderer, because this feature requires shaders. Since calculating lights and shadows can be heavy, Phaser 3 runs these on the GPU. To reference the lighting system, every scene has by default a LightsPlugin populated, which is accessible via this.lights. This is the interface for the scene to manipulate the lighting layer.

Don’t forget there is only one lighting layer per game. For example, lights created within scene A will affect sprites, which are added to the lighting layer by scene B. The layer is drawn onto the canvas before any scene, so every sprite not belonging to it will be rendered on top of it. The forward lighting has the limit of 10 maximum visible lights at once. This limit is hardcoded to Phaser. If you need more you should compile Phaser by yourself and change LIGHT_COUNT in phaser/src/renderer/webgl/pipelines/ForwardDiffuseLightPipeline.js.

Example

Edit on Codepen.io

How to use the lighting layer?

The first step is to tell the light system to start up. This needs to be called once for the game and not for each scene.

this.lights.enable();

Afterwards, the system is ready to create some lights. Be aware the limit of lights

this.lights.addLight( x, y, radius, rgb, intensity );

You may add a global light source, which is applied to all objects automatically. This light ignores any normal maps.

this.lights.setAmbientColor(rgb);

By default, no game object receives light and shadows. They completely ignore the lighting system. Therefore, you need to add every game object, which should be affected by it, to the lighting rendering pipeline.

this.add.image(x, y, key).setPipeline('Light2D');

Additionally, the sprites can now make use of normal maps. This allows you to control, how the sprite receives lights and can give a sort of 3D feeling. To achieve this, you can provide the LoaderPlugin a second URL for your sprite, which will be automatically imported as the corresponding normal map.

this.load.image(key, [image-path, image-normal-path]);

Now it is time to manipulate the lights during the runtime. The first approach to this is by storing a reference to the lights to manipulate.

light = this.lights.add(..);
light.x = 100;

But you don’t need to store the reference to all lights. You can easily iterate over all existing ones.

this.lights.forEachLight( callback(light));

To get an array with all lights, which affect the lighting of a camera, such as “this.camera.main”, you can call “cull”. It performs a range check between the camera and light, so all not visible lights get excluded from the list.

this.lights.cull(camera);

The example from above

class Foreground extends Phaser.Scene {
  constructor() {
    super({ key: "foreground", active: true });
    this.lamps = [];
  }

  preload() {
    this.load.image("logo", [
      "//jwiese.eu/assets/BlogLogo.png",
      "//jwiese.eu/assets/BlogLogo-n.png"
    ]);
  }

  create() {
    this.lights.enable().setAmbientColor(0x111111);
    console.log(this.lights.getMaxVisibleLights());
    
    this.add
      .image(360, 300, "logo")
      .setOrigin(0.5)
      .setPipeline("Light2D");

    //centerlight
    this.lights.addLight(400, 300, 300).setIntensity(0.5);

    //manipulation by tween
    this.tweens.add({
      targets: [
        this.lights.addLight(0, 0, 300),
        this.lights.addLight(720, 0, 300),
        this.lights.addLight(0, 600, 300),
        this.lights.addLight(720, 600, 300)
      ],
      intensity: {
        value: 2.0,
        duration: 500,
        ease: "Elastic.easeInOut",
        repeat: -1,
        yoyo: true
      }
    });
    for (let i = 0; i < 8; i++) {
      let color = 0x0a0a0a | (Math.random() * 0xffffff);
      this.lamps.push(
        this.lights
          .addLight(360, 300, 150)
          .setColor(color)
          .setIntensity(2.5)
      );
    }
  }

  update(time, delta) {
    let t = time / 1000;

    for (const [i, value] of this.lamps.entries()) {
      let position = t * (i + 1);
      value.x = 360 + Math.sin(position) * 200;
      value.y = 300 + Math.cos(position) * 200;
    }
  }
}

class Background extends Phaser.Scene {
  constructor() {
    super({ key: "background", active: true });
    this.background;
    this.backlight;
  }

  preload() {
    this.load.image("background", [
      "//jwiese.eu/assets/Scifi_Hex_Wall_Albedo.jpg",
      "//jwiese.eu/assets/Scifi_Hex_Wall_normal.jpg"
    ]);
  }
  create() {
    this.scene.sendToBack();
    this.background = this.add
      .image(360, 300, "background")
      .setScale(0.4)
      .setPipeline("Light2D");
  }
}

const config = {
  type: Phaser.WEBGL,
  width: 720,
  height: 600,
  parent: "example1",
  scene: [Foreground, Background]
};

const game = new Phaser.Game(config);