glslifyでGLSLをモジュール化しよう

いやぁ、今年のWebGL Advent Calendarはレベルが高いですね・・・
全然ついていけないので、ちょっと箸休めにシェーダーのビルド周りのお話をしてみます。

最近、[Deja vu | KAMRA](https://kamra.invisi-dir.com/)の[Gitレポジトリが全面公開](https://github.com/dot-by-dot-inc/KAMRA-Deja-Vu)されてたので、お宝探しに中を覗いていたのですが、その中で[glslify](https://github.com/stackgl/glslify)が使われていまして、コレは気になってたヤツ！だったのでちょっと色々試してみました。

一言で言えば、glslにrequireの仕組みを導入して、主に関数をモジュール化して別ファイルに分ける事が出来るやつですね。
node.jsで動きます。


## とりあえずコマンドラインで使ってみる

### glslifyのインストール
とりあえず、グローバルインストールで説明します。

```
npm install -g glslify
```

### glslify コマンドを実行

```
glslify input.glsl -o output.glsl
```

これで、カレントディレクトリのinput.glslからoutput.glslが書き出されます。

これだけだとサッパリなので、実際にモジュールを使ったinput.glslを書いて説明していきます。

## npmで公開されているモジュールを使ってみる
まずは、npmで公開されているモジュールを使ってみます。
サンプルとして擬似乱数を生成する関数のglsl-randomを用います。

ちなみに、npmで公開されているモジュールを探すには[stackgl/packages](http://stack.gl/packages/)の「Shader Components」を参照すると良いと思います。
割とよく使いそうなヤツが揃っていますね。


### モジュールのインストール

どこかのプロジェクトフォルダ内にglsl-randomをローカルインストールします。

```
npm install glsl-random
```

### モジュールのrequire

フラグメントシェーダーで擬似乱数によるモノクロのノイズ模様を描画するサンプルです。

```glsl:input.glsl
#pragma glslify: random = require(glsl-random)

uniform vec2 resolution; // 描画サイズ

void main(){
	gl_FragColor = vec4(vec3(random(gl_FragCoord.xy / resolution.xy)), 1.0 );
}
```

`#pragma glslify:` がglslifyに対する処理を記述する部分です。
ここでは random という名前でglsl-randomをrequireしています。

そして、そのmain関数内でそのrandomを使って擬似乱数を生成して gl_FragColor に色として渡します。

### glslifyした出力結果

先ほどのコマンドでinput.glslからoutput.glslを書き出すとこんな結果になります。

```glsl:output.glsl
#define GLSLIFY 1
highp float random(vec2 co)
{
    highp float a = 12.9898;
    highp float b = 78.233;
    highp float c = 43758.5453;
    highp float dt= dot(co.xy ,vec2(a,b));
    highp float sn= mod(dt,3.14);
    return fract(sin(sn) * c);
}

uniform vec2 resolution; // 描画サイズ

void main(){
	gl_FragColor = vec4(vec3(random(gl_FragCoord.xy / resolution.xy)), 1.0 );
}
```

見事にrequireした部分が実際の関数に置き換わっていますね！

`#define GLSLFY 1`
が先頭に自動追加されてますが、恐らくglslifyが何かに使っているのだと思います(適当)。

## 自前のモジュールを使ってみる

先ほどは公開されているモジュールを使いましたが、今度は自分でモジュールを作ってみます。

### モジュールの作成

以下は、思いつきで作ったサンプルです。
与えられた-1.0から1.0の値を0.0から1.0に変換します(念のためclampで切り詰める処理も)。

```glsl:zero-one.glsl
float zero_one(float t) {
    return clamp(t, -1.0, 1.0) * 0.5 + 0.5;
}

#pragma glslify: export(zero_one)
```

関数を作って、 `#pragma glslify:` でexportをしておけば大丈夫です。
これを、zero-one.glslという名前でinput.glslと同じフォルダに置いておきます。

### モジュールのrequire

続いて、このzero-one.glslをinput.glslにてrequireしてみます。

```glsl:input.glsl
#pragma glslify: zero_one = require("./test.glsl")

uniform vec2 resolution; // 描画サイズ

void main(){
	gl_FragColor = vec4(vec3(zero_one(sin(gl_FragCoord.x / resolution.x))), 1.0 );
}
```

先ほどはnpmにて公開されているモジュールを使ったので、`require(モジュール名)`だけでいけましたが、
今回はnpmにない手元のファイルを使うので相対パスで指定します。
クォートで囲まなかったり、`./`がないとエラーになったので注意が必要です。

### glslifyした出力結果

これをコマンドラインでglslifyすると、こんな結果になりました。

```glsl:output.glsl
#define GLSLIFY 1
float zero_one_1540259130(float t) {
    return clamp(t, -1.0, 1.0) * 0.5 + 0.5;
}

uniform vec2 resolution; // 描画サイズ

void main(){
	gl_FragColor = vec4(vec3(zero_one_1540259130(sin(gl_FragCoord.x / resolution.x))), 1.0 );
}
```

基本的には公開モジュールと同じような結果になりましたが、1点だけ異なるのが関数名に謎の接尾語が付いている点です。
名前の重複を防ぐための措置でしょうか・・・？

何はともあれ、こんな感じで自分が作った関数を別ファイルに分割出来るので、再利用が楽になりますね。

## glslify-import

glslifyは基本的に関数をexportしてrequireするものですが、[glslify-import](https://github.com/hughsk/glslify-import)を使うと単に別ファイルのGLSL構文をそのまま持ってこれます。
要はテンプレート機能ですね。

### glslify-importのインストール

プロジェクトフォルダ内にglslify-importをローカルインストールします。

```
npm install glslify-import
```

### importさせるファイル

試しによく使うattribute変数とvarying変数の宣言をまとめてみました。
template.glslというファイル名でinput.glslと同じフォルダにおいておきます。

```glsl:template.glsl
// よく使うattribute変数
attribute vec3 position;
attribute vec4 color;
attribute vec2 uv;
attribute vec3 normal;

// よく使うvarying変数
varying vec3 vPosition;
varying vec4 vColor;
varying vec2 vUv;
varying vec3 vNormal;
```

### importするファイル
後はコレを `#pragma glslify:` を使ってimportさせるだけです。

```glsl:input.glsl
#pragma glslify: import("./template.glsl")

uniform mat4 mvpMatrix;

void main(){
    vColor = color;
    vNormal = normal;
    vUv = uv;
    gl_Position = mvpMatrix * vec4(position, 1.0);
}
```

### importした出力結果
ちゃんとテンプレートしてますね。

```glsl:output.glsl
#define GLSLIFY 1
attribute vec3 position;
attribute vec4 color;
attribute vec2 uv;
attribute vec3 normal;

varying vec4 vColor;
varying vec2 vUv;
varying vec3 vNormal;

uniform mat4 mvpMatrix;

void main(){
    vColor = color;
    vNormal = normal;
    vUv = uv;
    gl_Position = mvpMatrix * vec4(position, 1.0);
}
```

## 【おまけ】gulpプラグインもどきを作ってみた

glslifyはBrowserifyとwebpackをサポートしているみたいですが、残念ながら自分はこれらを使っていません。
で、gulpで使えたら良いなぁと思って探してみたのですが、どうやら無さそうでした・・・

というわけで、見よう見まねでgulpプラグインもどきを作ってみたので、おまけで紹介しておきます。

```js:gulp-glslify.js
// gulpプラグインで必要なモジュール
var through2 = require('through2');
var gulpUtil = require('gulp-util');
var PluginError = gulpUtil.PluginError;

// glslifyとそのプラグイン的なの
var glslify = require('glslify');
var glslifyImport = require('glslify-import');
var glslifyHex = require('glslify-hex');

var PLUGIN_NAME = 'gulp-glslify';

function gulpGlslify(options) {

    if (!options) options = {};
    // 文字列を扱う前提で inline オプションは true
    options.inline = true;
    if (!options.transform) options.transform = [];
    // glslify-import と glslify-hex を追加
    options.transform.unshift(glslifyImport, glslifyHex);

    var stream = through2.obj(function (file, enc, cb) {

        // 対象ファイルから相対パスにするため
        options.basedir = file.history[0].split("/").slice(0, -1).join("/");

        if (file.isNull()) {
            cb(null, file);
            return;
        }

        if (file.isStream()) {
            cb(new PluginError(PLUGIN_NAME, 'Streaming not supported'));
            return;
        }

        var _this = this;
        if (file.isBuffer()) {
            glslify.bundle(file.contents.toString(), options, function(error, glsl, files) {
                if (error) {
                    cb(new PluginError(PLUGIN_NAME, error));
                    return;
                } else {
                    file.contents = new Buffer(glsl);
                    _this.push(file);
                    return cb();
                }
            });
        }

    });

    return stream;
}

module.exports = gulpGlslify;
```

先ほど説明した glslify-import に加えて [glslify-hex](https://github.com/hughsk/glslify-hex)を自動的に使うようにしてみました。
glslify-hex はGLSL中に `#336699` があると `vec3(0.2, 0.4, 0.6)` みたいに自動変換してくれます。

とりあえず適当な場所に置いて、相対パスでrequireすれば使えるようになっています。
npmで公開するかどうかはちょっと考え中・・・(やったことないので不安)

```js:gulpfile.js
var gulp = require("gulp");
var glslify = require("./gulp-glslify");

gulp.task("gulp-glslify", null, function() {
    gulp.src("./src/glsl/**/*.{vert,flag}")
        .pipe(glslify())
        .pipe(gulp.dest("./dest/"));
});
```

こんな感じでいつも通り使えます。

事前に glslify, glslify-import, glslify-hexは必ずインストールしておいてください。
~~また、through2 と gulp-util は他のgulpプラグインが使っていれば既に入ってる可能性がありますが、無ければこれらもインストールする必要があります。~~
また、through2 と gulp-utilも必ず先にインストールしておいてください。

<br>

それでは、良きGLSLライフを！
明日は[WebGL総本山](http://webgl.souhonzan.org/)の[@doxas](https://twitter.com/h_doxas) さんです！