Edited at

pixi.jsのカスタムフィルターでGLSLを学んでみる


はじめに

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

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

glitch2-256-11f.gif

グリッチエフェクトの例。なぜかホラー風味...

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

最近正式リリースされたv5でもほとんど通用しますが、若干の違いはあります(後述)。


バージョン詳細


  • pixi.js v4.8.7

  • pixi.js v5.0.2


準備

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 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.8.7/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

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


v4とv5の違い(調査中)

API上はuniformの定義の仕方がシンプルになったぐらいで、ほとんど一緒のようです。


Myfilter.v5.js

    super(

// vertex shader
null,
// fragment shader
fragmentSrc,
// uniforms
{
// time : { type: '1f', value: 0.0 }, // v4
time: 0.0, // v5
}
);

内部的にはGPUとのやり取りを減らすことで処理が高速化されてたりするらしいです。

他にもv4ではtextureCoordの正規化にちょっとしたおまじないが必要だったりしましたが、それが不要になりました。

参考:v5 実行例