いやぁ、今年のWebGL Advent Calendarはレベルが高いですね・・・
全然ついていけないので、ちょっと箸休めにシェーダーのビルド周りのお話をしてみます。
最近、Deja vu | KAMRAのGitレポジトリが全面公開されてたので、お宝探しに中を覗いていたのですが、その中で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の「Shader Components」を参照すると良いと思います。
割とよく使いそうなヤツが揃っていますね。
モジュールのインストール
どこかのプロジェクトフォルダ内にglsl-randomをローカルインストールします。
npm install glsl-random
モジュールのrequire
フラグメントシェーダーで擬似乱数によるモノクロのノイズ模様を描画するサンプルです。
#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を書き出すとこんな結果になります。
#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で切り詰める処理も)。
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してみます。
#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すると、こんな結果になりました。
#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を使うと単に別ファイルのGLSL構文をそのまま持ってこれます。
要はテンプレート機能ですね。
glslify-importのインストール
プロジェクトフォルダ内にglslify-importをローカルインストールします。
npm install glslify-import
importさせるファイル
試しによく使うattribute変数とvarying変数の宣言をまとめてみました。
template.glslというファイル名でinput.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させるだけです。
#pragma glslify: import("./template.glsl")
uniform mat4 mvpMatrix;
void main(){
vColor = color;
vNormal = normal;
vUv = uv;
gl_Position = mvpMatrix * vec4(position, 1.0);
}
importした出力結果
ちゃんとテンプレートしてますね。
#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プラグインもどきを作ってみたので、おまけで紹介しておきます。
// 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を自動的に使うようにしてみました。
glslify-hex はGLSL中に #336699
があると vec3(0.2, 0.4, 0.6)
みたいに自動変換してくれます。
とりあえず適当な場所に置いて、相対パスでrequireすれば使えるようになっています。
npmで公開するかどうかはちょっと考え中・・・(やったことないので不安)
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も必ず先にインストールしておいてください。