【連載】#5 twigl.app で始める GLSL クリエイティブコーディング! #つぶやきGLSL に学ぶコード圧縮 minify テクニック
コードを短く記述する文化
twigl.app で始める GLSL クリエイティブコーディング、第五回となる今回のテーマは「GLSL の minify をやってみよう」です。
Twitter で #つぶやきGLSL のハッシュタグなどを見ていると、本当にこんなに短いコードでこんな絵が出るの? と思うような作例がいくつも投稿されています。それらの中には、Demoscene と呼ばれるカルチャーで名を知られているクリエイターやコーダーの作品もたくさんありますね。
これは、おそらく「1ツイートに収まるように短く GLSL を書く」という行為が「Demoscene 界隈にもともと根付いている習慣・風習と感覚的に近い」からだと思います。
今回は、そのあたりの Demoscene の風土とは……みたいなところも含めて紹介しつつ、GLSL を短く記述することの技術的な観点からのコツなども紹介できたらいいかなと思います。
また、完全に偶然なのですが @notargs さんもコードの minify に関するブログ記事を公開されたようなので、こちらも要チェックです。
つぶやきGLSLで今すぐ使えるシェーダーminifyテクニック11選 - のたぐすブログ
制限があるからこそ燃えるクリエイティブ
Demoscene(デモシーン)について予備知識がゼロ、ということであれば、日本語で書かれた記事だと以下のものがわかりやすいかもしれません。
デモシーンにようこそ!超低容量プログラミングで遊ぶカルチャー CEDEC
上記の記事の冒頭に動画付きで紹介されている「Elevated」という作品は、いわゆる 4k intro の作品として伝説となっている超有名な作品なのですが…… 実際に動画を見て、どんな印象を抱くでしょうか。
3DCG がごく一般的になっている現代では「ふーん、きれいな CG やな~」くらいの感想を抱くひとが多いかも知れませんが、この作品は たった 4 キロバイトの実行ファイル(exe)を叩くとこの絵と音が出る と聞かされてから見てみると、どうでしょう。
絵と、音が、たった 4 キロバイトの実行ファイル1つから、全て漏れなく動的に生成されてフルスクリーンでこれが再生されるんですよ? 意味がわからないですよね…… そう考えると、当時すごく話題になったのも、うなずけるのではないでしょうか。
このように、デモシーンの界隈ではかねてより「小さなファイルサイズ」や「短いコード」で多くの人が思いもよらないような「グラフィックスやサウンド」を生成することに対して、多くの称賛が集まる文化があります。#つぶやきGLSL のハッシュタグも、感覚としてはそれに近いものがあって「1つのツイートに収まってしまうほど短いシェーダー」から、驚くような絵が生成される様子は誰の目にもまるで魔法のように映るのではないでしょうか。
基本的なコンセプト
短いコードで絵作りや音作りを行うとは言っても、#つぶやきGLSL とデモシーンのイントロ作品などとは、そもそも想定しているプラットフォームがまったく異なります。
デモシーンにおけるイントロ作品などの場合、それらは Windows の実行ファイルなど、特定のプラットフォーム上で実行できる形式の「ファイル」として提供されます。もちろん、それに対応する環境を準備できるひとであれば、実行ファイルをダウンロードしてきて自分の環境でじっくり鑑賞するなんてことも可能です。
一方で、ウェブをプラットフォームとするシェーダーの文化だと、ウェブブラウザが実行環境を兼ねることになるので必然的にプログラミング言語としては JavaScript、API の種類は WebGL であり、シェーダー記述言語は GLSL になります。
twigl.app の場合も、これは同様ですね。
エディタ上で記述した GLSL をソースとして、WebGL でこれを動的にコンパイル及び実行することにより、ブラウザ上にシェーダーの描画結果などを描画します。
@wrightwrighter さんの #つぶやきGLSL 投稿作品
twigl.app の場合は、グラフィックス部分については GLSL Sandbox との互換性があるので……
classic モードであれば resolution
や time
の他、マウスカーソルの位置などが uniform 変数としてシェーダーに送られてきます。このあたりは、連載の第一回で説明したとおりです。
一方、サウンドシェーダーについては Shadertoy の方式に互換性を持たせていますので、twigl.app 上で mainSound
という名前の関数の中身を GLSL で記述することで、シェーダー由来の音(サウンド・ミュージック)を生成します。これには GLSL だけでなく WebAudio が内部的には使われていて…… 原理を説明するとちょっと長くなりそうなので詳細は省きますが、いわゆる 4k intro などに見られる「動的にプログラムでサウンドを生成する」ということも twigl.app を利用すれば実現できるわけですね。
twigl.app 自体は、文字数をカウントする機能が備わっていたり、パーマリンク・GIF アニメーションを生成できたりと、いくつかの機能を持っています。そういう意味では、なにも #つぶやきGLSL に見られるような「極めて圧縮された短いシェーダー」に限定することなく、普通にオンラインのシェーダーエディタとして利用できます。
あくまでも、短いコードを書くのにも使えるよ、というだけでそれしかできないような実装になっているわけではありません。
まずは自由に書くのがよい
このように twigl.app は「minify する前提のコードを書きやすい」とか「アニメーション GIF として結果を手軽に共有しやすい」というだけで、作成した本人が言うのもあれですけれどもなにも「圧縮したコード専用」というわけじゃありません。
ですから特に GLSL に不慣れなうちは、最初から短く書くことを前提にしないほうがよいでしょう。
まずは自由に、文字数は気にせず自分がわかりやすい形で記述していく。これが大事だと思います。
ある程度、シェーダーの記述に慣れてきて、いよいよ自分も #つぶやきGLSL に挑戦したいぞ! となってきたら、そこで初めてコードの圧縮(minify)について考えるとよいと思います。
本稿では、ここから minify のためのテクニックをいくつか紹介しますが、これらはあくまでも「特殊なプラットフォームにおける極めて使い道の限られたテクニック」として理解してもらって、参考程度に読んでもらうのがいいと思います。
実際、minify されたシェーダのコードというのはけして読みやすいものではありませんし、かなりトリッキーなことをやったりするので、あまり学習の初期の段階からそれを意識しすぎてしまうと害はあっても得はないと思います。ただまあ、文字数制限という一種の制約があることで、必然的に作品がミニマルデザインに寄る傾向があり、そのことが「これで完成だ!」と自身に区切りをつける理由になるというのはあるかもしれませんね。
まずは数値の記述方法などを見直そう
minify のテクニックで、誰にでも簡単にできる最も基本的なテクニックが、省略可能な半角スペースの除去や数値の記述方法の圧縮です。
GLSL に限らず JavaScript なんかでもそうですが、浮動小数点の値を記述するとき、整数部分や小数点以下の部分は状況によって省略した記述を行うことができます。
// 普通に書いた例
float f = 1.0;
vec2 v = vec2(1.0, 0.1);
vec3 w = vec3(3.0, 3.0, 3.0);
// 上記を minify した例
float f=1.;
vec2 v=vec2(1.,.1);
vec3 w=vec3(3.);
半角スペースも取り去ってしまっているので、かなり可読性が下がっているとは思いますが、少なくとも構文的にはエラーにはならない、正しい書式です。
ここで行っていることは、大きくわけて2つあります。
1つ目は、小数点を含む値(つまり float
型)のときに、小数点以下が x.0
のように 0 が1つのみとなる場合にその 0 を省略できるというもの。つまり 1.0
は 1.
と書けるわけですね。これで1文字削減できる。
2つ目は、それとは反対に整数部分が 0.x
のように 0 となる場合には、その 0 を省略可能であるというもの。こちらは 0.1
が .1
と書けるわけです。
このあたりは、可読性が著しく損なわれてしまうものの、ルールとしてはそれほど難しくはありませんし丁寧にやってやれば誰にでも簡単にできる範囲の minify だと思います。
また、GLSL では浮動小数点は指数表記もできますので、ちょっと桁数の多い数値を使いたい、という場合はそちらも検討してみるといいかもしれません。桁数が多ければ多いほど、恩恵が大きくなります。
// 普通に書いた例
float f = 10000;
// 上記を minify した例
float f=1e4;
変数宣言時にまとめて定義
また、変数を宣言するときに「極力同じデータ型はまとめて定義してしまう」というのも場合により有効です。
特に float
型は宣言するたびに半角スペース込みで6文字も使ってしまうので、極力宣言文の記載回数を少なくしたいところです。
そこで、カンマを使って複数の変数を一度に宣言してしまうことにより、文字数を省略するテクニックが有用となります。
// 普通に書いた例
float x = 0.0;
float y = 0.0;
float z = 0.0;
// 上記を minify した例
float x=0.,y=0.,z=0.;
かなり文字数が少なくできましたね。
また上記のような記述を見れば自ずとわかるかもしれませんが、実際問題 float
が3つある状態というのは vec3
が1つあるのと同じですよね。
それ以降のコードでどのように変数を使うのかにもよりますので一概には言えませんが、場合によっては float
型で変数を2~4つ宣言するのであればそれをひとつのベクトルとして vec3 v=vec3(0.);
のように書いてしまったほうがいい場合もあります。
後者のベクトルを使って宣言する方法では、宣言部分では確かに文字列を省略できるのですが、それをそのあとの処理で利用するときに v.x
とか v.y
のようにドット記号+スウィズル演算子方式で書かないといけないので、トータルで考えると省略したよりも文字数が増えたりすることもありますから、注意しましょう。
スウィズル演算子やベクトル型の特性を活用
ちょっと地味な技ですが、スウィズル演算子を上手に使って記述量を減らせる場面というのもあります。
たとえば……
// 普通に書いた例
vec4 v = vec4(1.0, 2.0, 3.0, 4.0);
vec4 w = vec4(v.w, v.z, v.y, v.x);
// 上記を minify した例
vec4 v=vec4(1.,2.,3.,4.),w=v.wzyx;
なんと2行文のコードが、もとの1行文程度の量にまで圧縮できてしまいましたね。もっと短い書き方ももしかしたらあるのかもしれんが……
やっていることは、全然難しくはないと思います。スウィズル演算子は順序を変えて組み合わせたりすることもできるので v.xxxx
とか、あるいは v.xyxy
とか、自由に値を入れ替えてベクトルの各要素を表現できます。
また、第四回で説明したように、ベクトルはスカラーとの四則演算を行うことができ、その式が評価された結果は四則演算の対象となったベクトルと同じ要素数になります。ですから以下のような書き方が可能です。
// 普通に書いた例
float f = 10.0;
vec3 v = vec3(1.0, 2.0, 3.0);
vec3 w = vec3(v.x * f, v.y * f, v.z * f);
// 上記を minify した例
float f=10.;
vec3 v=vec3(1.,2.,3.),w=v*f;
これも、やっぱり冷静になって落ち着いて読み解いていけばそれほど難解ということもありませんよね。
たぶん、パッと見たときの印象が妙に窮屈なので気持ち的に圧倒されることはあるかもしれませんが、実際に起っていることはそんなに複雑な数学というものでもありませんし、たぶんこれは慣れですね……
ビルトイン関数のデータ型を把握する
実は GLSL の関数のなかには「引数のデータ型が柔軟に指定できるもの」があります。
これらについては、詳しくは WebGL の公式のリファレンス・シートが役に立つと思います。
これをグッと下までスクロールしてやると、WebGL 1.0 で利用できる GLSL ES 1.0 のリファレンスが出てきます。そのなかで、たとえば Angle & Trigonometry Functons の節を見てみると以下のようになっていますね。
本連載でも何度も登場したサインやコサインのあたりを見てみるとわかりやすいですが、戻り値の型と、引数の型には、いずれも T
という記載があります。これはこの節の説明文を見ると T is float, vec2, vec3, vec4
とあります。
これはどういうことかというと、サインを計算するための sin
関数は T sin(T angle)
のように書かれていますので、これを日本語で説明するなら「sin という関数は float か、もしくは vec2~vec4 のいずれかを引数に取り、同じ型の戻り値を返す」ということになるでしょう。
ですから、次のような省略した記法が使えますね。
// 普通に書いた例
vec3 v = vec3(2.0, 4.0, 6.0);
vec3 w = vec3(sin(time * v.x), sin(time * v.y), sin(time * v.z));
// 上記を minify した例
vec3 v=vec3(2.,4.,6.),w=sin(time*v);
わざわざ float
1つ分に分解して考えなくても、まとめて vec3
型として扱うこともできるんですね。
このように、公式のリファレンス(つまり正式な仕様)上で変数の型がここで言う T
のように柔軟に指定できるようになっているのかどうかは、minify 云々は抜きにしても知っておいて損はありません。
これらは minify だけでなく、単純に効率よくコードを記述することにも役に立つ知識ですので、ぜひ少しずつでもいいですから実際に使いながら覚えていくことをおすすめします。
if 文や for 文のブロック構造
他のプログラミング言語でも同様に実装されているような、制御構文の「仕様上は許されているけどお手本どおりではない記法」を活用することも、シェーダーを minify する上では役に立つことがあります。
たとえば if
文や for
文のような、ブロック構造を持つ構文がわかりやすい例でしょう。
// 普通に書いた例
if (a < b) {
c += d;
e = c + f;
}
// 上記を minify した例
if(a<b)c+=d,e=c+f;
if
のような制御構文は、else 節が存在しない場合大括弧( {}
)を省略できます。
また if
文については複数の文をカンマでつなぐように連続で記述できます。
for
文についてはさらにトリッキーなことができて、一般的には変数をインクリメントするために利用する式の部分を無理やりシェーダのロジックの一部に組み込んでしまうような書き方ができます。
はっきり言って、読むのかなり大変なんですが、構文的にはアリということですね。ただし、この記法は GLSL ES 3.0 でないと駄目っぽいので、その点は注意です。(twigl.app であれば classic (300 es)
などのモードを選択している必要があります)
// 普通に書いた例
for(int i = 0, j = 0; i < 9; ++i){
j += i;
}
// 上記を minify した例
for(int i,j;++i<9;)j+=i;
実際に for
文の中身でやっていることにまったく意味が無いのでちょっとわかりにくいかもしれないけども……
ここではその構文というか、仕組みをよく観察してみてください。
要は for
文というのはその宣言文が3つの式で成り立っていて for(初期化; 条件; 加算処理)
みたいな感じで通常は定義すると思うのですが、ここで紹介している minify のテクニックでは「初期化式で複数の変数を宣言」してみたり、あるいは「条件部分でインクリメントも一緒に行う」ようにしていたり、先程の if
文の例と同じように大括弧を省略していたりします。
一見すると何が起こってるのかわかりにくいかもしれないですが、1文字削るのにも神経をすり減らす minify の世界では、こういうテクニックも役に立つときが結構あります。
uniform は使わないなら消してしまえる
さて、その他 twigl.app ではこんなこともできるよという一例として、そもそも不要な宣言文は消してしまうというのもテクニックの1つと言えるかもしれません。
たとえば、マウスカーソルの位置を使った表現が不要であれば uniform vec2 mouse;
という一文は、削除してしまっても問題ありません。なんなら、画面の解像度だっていらねえや! ということであれば uniform vec2 resolution;
だって消してしまっても問題はありません。(めちゃくちゃ文字数を削減できる)
このように、本来あるべき uniform 変数を GLSL 側で削除してしまった場合というのは、それを動かしている CPU 側(ここでは JavaScript)では、本来取得できるはずの参照(C 言語のポインタのようなもの)がなくなってしまったりはするものの、実行できないほどのクリティカルなエラーにはならない場合が多いと思います。
CPU の実装側(JS など)のほうでは、本来 uniform 変数として mouse
や resolution
が存在するつもりで処理が書かれてはいるでしょう。ただ、それを省略された場合に例外をスローにしないように実装が組まれていれば、少なくとも GLSL 的には構文が間違っているわけでもないのでエラーにはならないわけですね。
twigl.app の geeker モード
twigl.app には3つのモードがあり、geeker モードを選択すると、一切の uniform 宣言が不要になります。
しかし、この「geeker モードの uniform 宣言が不要になる」ということと、上記で説明している「要らないなら削除しちゃえ」というのは根本的にはまったく異なるので、混同しないように注意が必要です。
geeker モードでは、ユーザーがエディタ上で記述した GLSL のソースコードを取得して、コンパイルに掛ける前に「宣言文を文字列として結合してからコンパイルする」ということを行うことで、uniform 宣言文などを省略できるようにしています。つまりソフトウェア的に解決しているわけですね。
一方で、上記で説明したような「不要な uniform 変数の宣言文は消してしまう」というのは、純粋に GLSL のソースコード上から宣言文自体を消してしまっています。ですから、たとえばいったん resolution は消してしまったけどやっぱり使いたいから…… みたいな状況に至った場合、きちんと uniform 宣言文のほうも復活させてやらないと、CPU 側から送られてくる入力を受け取ることはできなくなります。
GLSL をしっかり勉強していればわかることではあるのですが……誤解があるといけないなと思ったので、一応補足でした。
あと1文字が削れねえ!(それ精度修飾子でできるよ)
uniform 宣言の文以外にも、実はもうひとつ、文字列を削減できる箇所が冒頭の宣言文に隠れています。
該当の部分は precition highp float;
のところですね。
第一回でも解説しましたが、これは「浮動小数点にどのような精度を求めるか」を指定する精度修飾子による宣言文なのですが……
精度修飾子に指定できる選択肢には highp
だけでなく、中位を表す mediump
と下位を表す lowp
があります。
ご覧の通り highp
は5文字ですが lowp
は4文字です。
もうどこにも削れるところがねえ! 駄目だ! みたいなときは、こういう姑息な技を使って文字列を削減することもできるんですね……(姑息だけど……背に腹は代えられない)
#define マクロでさらなる高みへ
#define
マクロについても、本連載で以前に簡単に説明しましたが、これを持ち出すとそれこそいろんなトリックが使えるようになります。
バリエーションが多いので、その全てを網羅することはちょっと難しいのですが……
たとえば、頻出する宣言の型などをマクロにしておくと結構役に立つ場面があるかもしれませんね。以下のようなコードを例に、考えてみます。
precision highp float;
uniform float time;
void main(){
float r = cos(time * 2.0);
float g = cos(time * 3.0);
float b = cos(time * 4.0);
vec3 c = vec3(r, g, b) * 0.5 + 0.5;
gl_FragColor = vec4(c, 1);
}
これを見ると float
と書かれている箇所が少なくとも5箇所はありますよね。
ですから、これは次のように #define
を使ってリプレイスできます。
#define f float
precision highp f;
uniform f time;
void main(){
f r = cos(time * 2.0);
f g = cos(time * 3.0);
f b = cos(time * 4.0);
vec3 c = vec3(r, g, b) * 0.5 + 0.5;
gl_FragColor = vec4(c, 1);
}
ただしこの方法にはちょっとした罠がありまして……
リプレイスを行う前の文字数が211文字、リプレイス後は207文字です。つまり、見た目ほど、実際には圧縮される効率は高くありません。
これは #define f float
+ 改行文字1文字、というのが単純に結構な文字数を使っているからですね。あえて多少の文字数を消費してでもマクロを登録すべき場面なのかどうか、割と慎重に見極める必要があります。
ただ、たとえば for
文を回して処理するようなコード自体をマクロとして登録しておき、それを繰り返し呼び出す、みたいな使い方をすると for
文自体の宣言は最初のマクロの部分で1回のみで済ませることができ、かつ繰り返し処理を複数回実行可能にしたりできるので、フラクタル的な再帰っぽい書き方をするコードでは効果を発揮します。
このように #define
についてはちょっと上級者向けっていう感じですね。
その他
その他、かなり上級者向けな GLSL コードの圧縮テクニックというのは無数にあります。
たとえば「回転行列を定義してベクトルの回転を行う」というコードに対しては以下のような書き方ができます。
// 普通に行列定義して回転する場合
vec2 p = gl_FragCoord.xy;
float s = sin(time);
float c = cos(time);
mat2 m = mat2(c, s, -s, c);
vec2 q = p * m;
// minify 版
vec2 q=gl_FragCoord.xy*mat2(cos(time+vec4(0,33,11,0)));
これはあくまでも「近似」なので、結果が完全に同じになるわけではありませんが……
できる限りコードを短くしたい #つぶやきGLSL のような利用シーンでは、有用なテクニックだと思います。
では、どうしてこのようなコードで行列による回転処理が近似できるのか……
コードを見ただけで、わかるひとにはわかるというか「なるほどなッ!」ってなると思いますし、もしこれを読んでいる方がコードだけを見てその原理がわからないとしたら、ちょっと残酷なようですが「今はそのときではない」ということなんだと思います。
まず「回転行列」あたりで検索してそっちの知識をキチンと身につけるべきです。※というスパルタな原稿を書いてしまったのですが、冒頭で紹介した notargs さんのブログ記事の方では原理を解説してくれているので、そちらをチェックしてみてもよいかもしれません。
twigl.app を使ったコード圧縮や #つぶやきGLSL のようなカルチャーは、たしかに楽しいですし、かっこいいですし、めちゃくちゃ痺れますしエモいですけれども、それを楽しむためにはやっぱりどこかで 基礎を押さえる必要がある んじゃないかなと私個人は思います。そして基礎を押さえているからこその「楽しさ」や「驚き」が確かにあって、それを感じることができるのは「その場に立つことができたひとの特権」だと思います。
くれぐれも勘違いしないでほしいのは「その特権は基礎を学ぶだけで手に入る」ものであり「選ばれた天才や神々たちだけのものじゃない」ということです。
ベクトルとか、行列とか……確かに難しいですよね。わかります。
私もぶっちゃけ、最初は全然ようわからんかったくちですし……
でも、今すぐでなくてもいいです、少しずつでも構わないですから、興味があるならちょっとずつでも勉強したり挑戦したりしてみましょう。どんなに鈍足でも諦めずに自分のペースで理解を深めていけば、必ずさきほどの minify された回転の近似の仕組みも自分のちからで読み解けるようになるはずです。
最後に
約一週間に渡って、GLSL の基礎の基礎から解説してきた連載ですが、どうですかね…… 少しは誰かの役に立ったでしょうか。
GLSL や WebGL は、それを知らない人から見ると本当に魔法のようで、かといって実際にやってみようとすると想像していた以上に険しい道のりであると感じてしまう場合も多く、満足に使いこなせるようになるには結構な努力や時間が必要になります。
でも、それはどんなことに対しても、同じなんじゃないかなと個人的には思います。
よく、巷では(素人を食い物にする詐欺師まがいの人たちによって)「プログラマーは簡単に稼げる職業だ」みたいなニュアンスで語られたりしますが、たぶん実際にプログラマーとして働いているみなさんからしてみれば、そんなに簡単なもんじゃねえよパソコン用語の基礎から勉強して出直してこい! みたいに思ったりすることもあると思います。
尺度は違うけど、GLSL も似たようなもので、かっこいい絵を出したいなら基礎を勉強しなくちゃだめなんです。こればっかりは、本当に何ていうか…… 単なる事実です。厳しい現実とかじゃなくて、単なる事実なんですよね。
それでもやっぱり、少しでも多くの人に GLSL や WebGL の魅力を伝えていきたいですし、一人でも多くの人たちと GLSL を通じて楽しい体験を共有できたら、こんなに幸せなことはないよなとも思います。挫折してしまう人は少ないほうがいいですし、私がそのためになにかできるなら、できる限りのことはしたいなとも思います。
なかなか最初は難しいことも多いと思いますが、ぜひ自分のペースで、少しずつでも挑戦してみましょう。
twigl.app は GitHub でソースも公開しています。もしよかったら、Star や Pull Request いただけましたら作者は大変喜びます。ステキな GLSL ライフのお供に、twigl.app や今回の連載が少しでも役に立ったなら嬉しいかぎりです。
リンク:
【連載】#1 twigl.app で始める GLSL クリエイティブコーディング! まずは GLSL の基本を理解しよう
【連載】#2 twigl.app で始める GLSL クリエイティブコーディング! スクリーン座標正規化を理解する
【連載】#3 twigl.app で始める GLSL クリエイティブコーディング! よく見る GLSL のビルトイン関数を知る