Phaser3でのShaderを利用したエフェクト

9 min read読了の目安(約8400字 2

実況動画でキャラクター立ち絵にエフェクトかける感じ、楽しいです。
Phaserでもshaderを使って気楽に素材にエフェクトをかけて楽しみます。

とりあえず動いてるのを見る

Phaserの機能をとりあえず試すなら公式のExamplesのサイトです。
ライブコーディング対応していますので、小さい実験などに大変便利です。

今回のShaderエフェクトに関する項目はこのサイトの/HOME/Rendererとなります。
この中では、シンプルな実装の「グレースケールフィルタ」の例が参考になります。

元々はカラー画像の「かにとあんこう」がグレー化されます。

公式のExamplesは便利ですが、世界中の方が見るせいか割とよく落ちます。
開発中に見れないと割と腹が立ちます!
GithubにExamplesの全コードがありますので、git cloneしてローカルでいつでも見れるようにしておくと便利です。素早く見れるし、腹も立ちません。

サンプルの実装を見る

Phaserのチュートリアルを終えているなら、ほとんど珍しいコードはないです。
注目すべきは以下の4箇所です。

import GrayScalePipeline from './assets/pipelines/GrayScale.js';
//(略)
const grayscalePipeline = this.renderer.pipelines.get('Gray');
this.fish = this.add.sprite(400, 300, 'fish').setPipeline(grayscalePipeline);
//(略)
const config = {
    pipeline: { 'Gray': GrayScalePipeline }
};

1.GrayScale処理クラスを読み込む。
2.GameConfigに'Gray'という名前で処理を登録。
3.RendererからGrayという処理のpipelineを呼び出す。
4.登録したパイプラインを画像に適用する。

やりたいeffectのglslをクラスを用意すればすぐに応用できそうです。

詳細

1.は後で書きますが、glsl処理をクラス化したものです。

2.で'Gray'という名称でRendererにpipelineを登録しています。
Rendererは…ここではhtmlでゲームを描画をするための基本的な仕組みと捉えます。
Phaserに限らず、コンピューターシステムでデータを元に画面に色を描画する場合は、
特定の手順を毎回行って画面に出す色を決めます。その手順をpipelineと呼びます。
このpipelineに自作処理を介入させると、簡単に描画内容全体へ影響を与えることができます。
この仕組みは、エフェクトをかけるのに最適です。

3.では新規に作成したpipelineをRendererから呼び出しています。

3.では実際に作ったpipelineをGameObject(画像)へ適用しています。
例えば適用先をSceneにすれば、画面全体に影響が出ます。

次に、GrayScale処理クラスを見ます。
公式Githubですとこちら

コード
const fragShader = `
precision mediump float;

uniform sampler2D uMainSampler[%count%];
uniform float gray;

varying vec2 outTexCoord;
varying float outTexId;
varying vec4 outTint;
varying vec2 fragCoord;

void main()
{
    vec4 texture;

    %forloop%

    gl_FragColor = texture;
    gl_FragColor.rgb = mix(gl_FragColor.rgb, vec3(0.2126 * gl_FragColor.r + 0.7152 * gl_FragColor.g + 0.0722 * gl_FragColor.b), gray);
}
`;

export default class GrayScalePipeline extends Phaser.Renderer.WebGL.Pipelines.MultiPipeline
{
    constructor (game)
    {
        super({
            game,
            fragShader,
            uniforms: [
                'uProjectionMatrix',
                'uMainSampler',
                'gray'
            ]
        });

        this._gray = 1;
    }

    onPreRender ()
    {
        this.set1f('gray', this._gray);
    }

    get gray ()
    {
        return this._gray;
    }

    set gray (value)
    {
        this._gray = value;
    }
}

1.glslコード本体
2.Phaser.Renderer.WebGL.Pipelines.MultiPipelineを継承したクラス定義

あまり大したことはやってません。
単にglslコードのラッパークラス、という印象です。
コイツを真似れば、glslコードのクラス化はすぐできそうです。

ここまで書いといてなんですが、このサンプルコードは若干実装が古いみたいです。
継承元のクラスの記述が公式ドキュメントにないので、一応動くけど推奨コードではないかもしれません。

TypeScriptのサンプルも見る

実装は難しくなさそう、という気持ちを得たので、次はTypeScriptのサンプルを見てみます。
TypeScript用にPhaserExamplesを移植している方がいるため、ありがたく参考にします。
簡単そうなのはmainカメラにshaderを適用したこの例(Phaser3/TypeScript/Camera)です。

RGBが目まぐるしく変わる、いわゆるゲーミング🎮エフェクトです。

コードで大事なのは以下ですね。

import CustomPipeline from './CustomPipeline'
//略

const renderer = this.game.renderer as Phaser.Renderer.WebGL.WebGLRenderer
this.customPipeline = renderer.addPipeline('Custom', new CustomPipeline(this.game))
//略
this.cameras.main.setRenderToTexture(this.customPipeline)

update(time: number, deltaTime: number)
{
//略
this.customPipeline.setFloat1('time', this.accumulatedTime)
this.accumulatedTime += 0.005

jsの場合と大して変わっていません。
1.Shaderクラスのimport
2.rendererへのpipeline登録
3.登録したpipelineをメインカメラに適用
4.時間で変化するEffectは、update()内で時間で変化する値をShaderに渡す。

TSの例では、addPipeline()を使ってメイン処理内でShaderを登録しています。
GameConfigを汚さないこちらの使い方の方が実践的な気がします。

あとは、時間変化系のshaderはupdate内で変数を渡すというやり方が一般的なようですのでこれは覚えておくといい感じですね。

Shaderクラスを見ます。

コード
import Phaser from 'phaser'

export default class CustomPipeline extends Phaser.Renderer.WebGL.Pipelines.TextureTintPipeline
{
	constructor(game: Phaser.Game)
	{
		super({
            game: game,
            renderer: game.renderer,
            fragShader: `
            precision mediump float;
            uniform sampler2D uMainSampler;
            uniform float time;
            varying vec2 outTexCoord;
            varying vec4 outTint;
            #define SPEED 10.0
            void main(void)
            {
                float c = cos(time * SPEED);
                float s = sin(time * SPEED);
                mat4 hueRotation = mat4(0.299, 0.587, 0.114, 0.0, 0.299, 0.587, 0.114, 0.0, 0.299, 0.587, 0.114, 0.0, 0.0, 0.0, 0.0, 1.0) + mat4(0.701, -0.587, -0.114, 0.0, -0.299, 0.413, -0.114, 0.0, -0.300, -0.588, 0.886, 0.0, 0.0, 0.0, 0.0, 0.0) * c + mat4(0.168, 0.330, -0.497, 0.0, -0.328, 0.035, 0.292, 0.0, 1.250, -1.050, -0.203, 0.0, 0.0, 0.0, 0.0, 0.0) * s;
                vec4 pixel = texture2D(uMainSampler, outTexCoord);
                gl_FragColor = pixel * hueRotation;
            }   
            `
        })
	}
}
``

かなり簡潔です。
1.Phaser.Renderer.WebGL.Pipelines.TextureTintPipelineを継承したクラスを作り
2.fragshader propertyにglslコードを入れる
これなら、なんとなく自作Shader Classも書けそうです。

自分のプロジェクトで動かしてみる

TSサンプルを見て完全に理解した気持ちになりましたので、次はこちらのPhaser3 + TypeScriptプロジェクトテンプレートを使って自ブロジェクトで動かします。

プロジェクトをcloneして、ビルドします。いつもの画面です。

先程のGameingEffectを使ってみます。
若干分かりづらいですが、PhaserLogoがGaming🎮PhaserLogoに昇格しています。

先程のTS Exampleのコードの簡単な移植です。

  • CustomPipeline.tsをコピーして、srcに配置
    • 冒頭のimport Phaserは不要なので削除
  • game.tsで呼び出し
    • customPipelineの登録
    • GameObject(PhaserLogo)へのpipeline設定

サンプルとの違いは、Cameraに適用していたpipelineをPhaserLogoに適用している点です。
抜粋すると以下のようになります。

create():void{
//略
	const logo = this.add.image(400, 70, "logo").setPipeline("Custom");

普通にsetしているだけです。
cameraへのPipeline適用とはmethod名が異なるぐらいですね。

完全なコードはこちらに置きました。

他の解説サイトを見てみる

PhaserのShaderチュートリアル記事だと、こちらの記事が参考になります。
ブロック崩しゲーム画面全体(カメラ)にEffectをかけています。

歪んだ状態でブロック崩しが動いている異常な世界です。
ShaderEffectの凄みが分かります!
実際のゲームに適用している解説内容なので、コアとなるコードを読み解くのはやや面倒です。

Typescriptで実装している日本語情報だとこちらが参考になります。

その他、ちょっと古いですがGLSL自体の入門にはこちらの日本語記事が丁寧です。

自作してみる

TSのCustomPipelineクラスの例を参考にすれば、glslからの実装に特に問題はないと思います。
練習問題としては、まずは参考サイトにあるものを自分のプロジェクトで動かしてみるのが良いでしょう。

試しに、掲示板で話されていたディザ合成フィルタをうちの猫画像に適用してみました。

😸ニャーン

その他

GameObjectとしてのShader

これは前回の記事の内容です。
Phaser的な説明になりますが、前回の記事の方法でのglslロードは、
「glslで記述された描画内容をGameObjectとして取り扱えるようにする」ということをやっていました。
GameObjectなので、拡大縮小、スケーリング、配置等ができます。

この方法は、基本の描画システムから独立して実施されるようで、コストがかかります。
そのため、公式documentにあるように、特別な演出目的で利用するケースに用いるのが良さそうです。

パイプラインをブロックするため、現在進行中のバッチ処理が中断されることを意味します。したがって、これらのゲームオブジェクトは慎重に使用する必要があります。完全にバッチ処理されたカスタムシェーダーが必要な場合は、代わりにカスタムパイプラインの使用を検討してください。ただし、背景や特殊なマスキング効果の場合、非常に効果的です。

Shader以外の演出方法

演出上のエフェクトとしては、Shaderを使わなくても簡易的なものがPhaserフレームワークに組み込まれています。
組み込み処理でできないレベルの演出をやるときにShaderを検討するのがよさそうです。

  • Tint (Objectの基本色変更)
  • Camera (マスク効果、フェードイン、フェードアウト、パン等)
  • tweeenによるalpha制御
  • ブレンドモード設定

終わり

Phaserは公式のexampleが多岐にわたるため、ゲームで演出したい内容と、その実装手段の紐付けがいまいち素直に行きません。
次の記事では、神サイト「Aviutilの優しい使い方」のような感じでエフェクト実装方法がexampleのどこにあるか探せるような内容をまとめたいと思います。