はじめに

最近pixi.js v3用のフィルタークラスがv4で動かない原因を探って色々勉強している内に、カスタムフィルタの作成はGLSLの学習に向いているかも、と思い始めました。

GLSL sandboxなどでは自前の画像が使えませんが(ですよね?)、pixi.jsのfilterなら用意したテクスチャはもちろん、Graphicsクラスで描いた図形などにもシェーダを適用できます。

glitch2-256-11f.gif
グリッチエフェクトの例。なぜかホラー風味...

個人的にはこういった画像に何がしらのエフェクトをかける、いわゆるポストプロセシングが楽しく、この学習手法が向いていると感じました。

以下、やり方などを紹介します。
pixi.jsのバージョンはv4.x系が対象となります。

準備

ES5流にpixi v4のフィルタークラスのテンプレートを書くと以下のような感じになると思います。

ES5流
MyFilter.js
PIXI.filters.MyFilter = function () {
  var fragmentSrc = [
    'precision mediump float;',
    'uniform sampler2D uSampler;',
    'varying vec2 vTextureCoord;',
    'void main (void) {',
    ' vec4 color = texture2D(uSampler, vTextureCoord);',
    ' gl_FragColor = color;',
    '}'
  ];

  PIXI.Filter.call(this,
    null, // vertex shader
    fragmentSrc.join('\n'), // fragment shader
    {} // uniforms
  );
};

PIXI.filters.MyFilter.prototype = Object.create(PIXI.Filter.prototype);
PIXI.filters.MyFilter.prototype.constructor = PIXI.filters.MyFilter;

fragmentSrc変数にシェーダ内容を記述した文字列を突っ込む必要がありますが、ES5では複数行の文字列を書くのにこのようにちょっとした工夫が必要で、ちょっと編集しにくいです。

これに対しGLSLを別ファイルに書き、バンドルツールでプリコンパイルするなどの方法がありますが、ES6(ES2015)のテンプレートリテラルを使ってGLSL部分を書き、エディタのシンタックス設定を変えることで手軽に編集しやすくすることもできます。
ついでに見た目をスマートにするため、class構文にしてみます。

ES6流
MyFilter.class.js
PIXI.filters.MyFilter = class MyFilter extends PIXI.Filter {
  constructor() {
    var fragmentSrc = `
      precision mediump float;
      varying vec2 vTextureCoord;
      uniform sampler2D uSampler;

      void main(void) {
        vec4 color = texture2D(uSampler, vTextureCoord);
        gl_FragColor = color;
      }
    `;

    super(
      null, // vertex shader
      fragmentSrc, // fragment shader
      {} // uniforms
    );
  }
};

(IEや旧android標準ブラウザ等の古い環境では対応していないことに注意)

その他のHTML部分等は以下のような感じです。

index.html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, user-scalable=no" />
    <meta name="apple-mobile-web-app-capable" content="yes" />

    <title>pixi.js filter</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/4.3.4/pixi.min.js" type="text/javascript" charset="utf-8"></script>
    <script src="./path/to/Myfilter.js" type="text/javascript" charset="utf-8"></script>
    <script src="main.js" type="text/javascript" charset="utf-8"></script>
  </head>
  <body>
  </body>
</html>
main.js
// canvasのサイズ(任意)
const SC_WIDTH = 400;
const SC_HEIGHT = 256;
const IMG_SRC = "画像のパスをここに書く";

const init = function(loader, resources) {
  // アプリ生成
  let app = new PIXI.Application(SC_WIDTH, SC_HEIGHT);
  document.body.appendChild(app.view);

  // スプライト追加
  let img = new PIXI.Sprite(resources.img.texture);
  app.stage.addChild(img);

  // フィルタを適用
  let myFilter = new PIXI.filters.MyFilter();
  app.stage.filters = [myFilter];
};

// 画像ロード完了後に初期化
PIXI.loader.add('img', IMG_SRC).load(init);

一応、今すぐ試したい人用にオンラインエディタ上のテンプレートを用意しました。
テンプレート
fragmentSrcの部分を改変するなどして遊びます。

フィルターを描いてみる

あとはひたすら純粋にGLSLを勉強して試すだけです。
GLSL自体についてはすでに良質な記事がたくさんあるので、そちらに解説を譲りますが、せっかくなのでいくつか例を紹介します。
例えば、テクスチャを赤っぽくしたい場合。

MyFilter.js
const fragmentSrc = `
      precision mediump float;
      varying vec2 vTextureCoord;
      uniform sampler2D uSampler;

      void main(void) {
        // テクスチャのピクセルデータ
        vec4 color = texture2D(uSampler, vTextureCoord);

        // 赤だけ定数にする
        color.r = 0.8;     
        gl_FragColor = color;
      }
    `;

実行結果
red-strong.png

uniform変数を渡して動的なエフェクトをかけてみる

シェーダに適宜uniform変数を渡すことで、動的にエフェクトを変えられます。
カスタムuniform変数を渡すには、フィルタークラスのコンストラクタ内で以下のように引数を指定します。

Myfilter.js
    super(
      // vertex shader
      null,
      // fragment shader
      fragmentSrc,
      // uniforms
      {
        time : { type: '1f', value: 0.0 },
      }
    );

time変数は経過時間を表します。
毎フレーム経過時間を渡すため、先程のmain.jsのinit関数内に以下コードを追加します。

main.js
// ~~ 中略
  app.ticker.add(function(){
    // 時間経過をシェーダに伝える
    myFilter.uniforms.time += app.ticker.elapsedMS * 0.001;
  });

time変数を使って、昔のブラウン管テレビの走査線みたいなエフェクトを再現するフィルタです。

MyFilter.js
const fragmentSrc = `
      precision mediump float;
      varying vec2 vTextureCoord;
      uniform sampler2D uSampler;
      varying vec4 vColor;

      uniform float time;

      void main(void) {
        vec2 cord = vTextureCoord;
        vec4 color = texture2D(uSampler, cord);
        float scanLineInterval = 1300.0; // 大きいほど幅狭く
        float scanLineSpeed = time * 5.0; // 走査線移動速度

        // 走査線
        float scanLine = max(1.0, sin(vTextureCoord.y * scanLineInterval + scanLineSpeed) * 2.0) * 1.5;

        color.rgb *= scanLine;

        gl_FragColor = color;
      }
    `;

実行結果
scanline.gif

ちょっとパラメーターを変えるだけかなり印象が変わったりして結構面白いのでぜひ遊んでみましょう。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.