🧑‍💻

GLSL最短チャレンジ #つぶやきGLSL

2021/12/19に公開

この記事は Akatsuki Advent Calendar 19日目の記事です。
前回はShader Errorさんの「UnityでRenderer Feature使ってみたい」でした。

#つぶやきGLSL

#つぶやきGLSLというチャレンジをご存知でしょうか。
Twitter上で行われている、GLSLシェーダーをできる限り短く書いて作品を投稿するハッシュタグです。ツイートしなければいけないので半角で280文字(-ハッシュタグ文字数)しか使えないという制限があります。

https://twitter.com/yuji_ap/status/1470772374505410563

最近このつぶやきGLSLにハマっているのでアドカレのネタにしようと思います。

GLSLの実行

今回GLSLはtwigl.appというサイトを使って記述&実行します。
表示されるエディタに以下のコードをコピペすると、レイマーチングによって球体が大量に並んだグラフィックが表示されるはずです。

precision highp float;
uniform vec2 resolution;

void main(){
  vec2 r = resolution;
  vec2 p = (gl_FragCoord.xy * 2.0 - r) / max(r.x, r.y);
  vec3 rayDir = normalize(vec3(p, 1.0));
  float depth;
  
  for(int i = 0; i < 99; i++){
    vec3 rayPos = vec3(rayDir * depth);
    rayPos = mod(rayPos, 4.0) - 2.0;
    float distToObj = length(rayPos) - 1.0;
    if(distToObj < 0.0001) break;
    depth += distToObj;
  }
  
  gl_FragColor = vec4(depth * 0.02);
}

この記事ではレイマーチングに関する説明は省きますが、気になる方は以下の記事を読むと良いかもです。
https://qiita.com/edo_m18/items/034665d42c562da88cb6


コードを短くしていく

先程のコードを再掲しますが、これはある程度読みやすいように書いたコードです。
そのため、全体で451文字かかりました。(twigl.appのコード左上に「○○ chars」という文字数表示があります)

precision highp float;
uniform vec2 resolution;

void main(){
  vec2 r = resolution;
  vec2 p = (gl_FragCoord.xy * 2.0 - r) / max(r.x, r.y);
  vec3 rayDir = normalize(vec3(p, 1.0));
  float depth;

  for(int i = 0; i < 99; i++){
    vec3 rayPos = vec3(rayDir * depth);
    rayPos = mod(rayPos, 4.0) - 2.0;
    float distToObj = length(rayPos) - 1.0;
    if(distToObj < 0.0001) break;
    depth += distToObj;
  }

  gl_FragColor = vec4(depth * 0.02);
}

つぶやきGLSLとしてツイートするためには270文字程度に収めなければならないため、ここからどんどん文字数を減らしていきます。

最短チャレンジスタート!

twigl.appのRegulationを変更する

早速チート技なのですが、twigl.appには省略記法が使える設定があります。
コード右側の「Regulation」がデフォルトでは「classic」になっていますが、これを「geekest(300es)」に変更。

どんな記法が使えるようになるかはこちらのREADMEに書かれています。

今回の例で言うと

  • 最初のfloat(浮動小数点数の精度)、resolution(画面解像度)の宣言が不要になる
  • main関数の記述が不要になる
  • 画面解像度は r で取る
  • gl_FragCoord(入力座標)は FC、gl_FragColor(出力色)は o と書く

これらを修正したもがこちら。この修正を行うことで、geekest(300es)でのコンパイルが通ります。

- precision highp float;
- uniform vec2 resolution;
- 
- void main(){
-   vec2 r = resolution;
-   vec2 p = (gl_FragCoord.xy * 2.0 - r) / max(r.x, r.y);
+ vec2 p = (FC.xy * 2.0 - r) / max(r.x, r.y);
  vec3 rayDir = normalize(vec3(p, 1.0));
  float depth;

  for(int i = 0; i < 99; i++){
    vec3 rayPos = vec3(rayDir * depth);
    rayPos = mod(rayPos, 4.0) - 2.0;
    float distToObj = length(rayPos) - 1.0;
    if(distToObj < 0.0001) break;
    depth += distToObj;
  }

-   gl_FragColor = vec4(depth * 0.02);
+ o = vec4(depth * 0.02);
- }

Regulationをgeekest(300es)にするだけで、なんと451文字→321文字まで減りました。
ただしチート技はこれで終わりで、ここからは地道に文字数を減らしていきます。

変数名を短くする

まずは簡単に思いつく変更として、変数名を1文字にします。
可読性全捨ての怒られそうな命名です。

  vec2 p = (FC.xy * 2.0 - r) / max(r.x, r.y);
- vec3 rayDir = normalize(vec3(p, 1.0));
- float depth;
+ vec3 e = normalize(vec3(p, 1.0));
+ float g;

  for(int i = 0; i < 99; i++){
-   vec3 rayPos = vec3(rayDir * depth);
-   rayPos = mod(rayPos, 4.0) - 2.0;
-   float distToObj = length(rayPos) - 1.0;
-   if(distToObj < 0.0001) break;
-   depth += distToObj;
+   vec3 q = vec3(e * g);
+   q = mod(q, 4.0) - 2.0;
+   float d = length(q) - 1.0;
+   if(d < 0.0001) break;
+   g += d;
  }

- o = vec4(depth * 0.02);
+ o = vec4(g * 0.02);

321文字→251文字
これでツイートできるようになりました!

しかしこれで満足せず、もっと短くしていきます。

余分な空白を消す

もう一つ簡単に思いつく短縮法として、a = b → a=b のように演算子や括弧周りの余分な空白を消して詰め詰めにします。
空行も消してしまいます。

- vec2 p = (FC.xy * 2.0 - r) / max(r.x, r.y);
- vec3 e = normalize(vec3(p, 1.0));
+ vec2 p=(FC.xy*2.0-r)/max(r.x,r.y);
+ vec3 e=normalize(vec3(p,1.0));
  float g;
- 
- for(int i = 0; i < 99; i++){
-   vec3 q = vec3(e * g);
-   q = mod(q, 4.0) - 2.0;
-   float d = length(q) - 1.0;
-   if(d < 0.0001) break;
-   g += d;
- }
+ for(int i=0;i<99;i++){
+   vec3 q=vec3(e*g);
+   q=mod(q,4.0)-2.0;
+   float d=length(q)-1.0;
+   if(d<0.0001)break;
+   g+=d;
+ }
- 
- o = vec4(g * 0.02);
+ o=vec4(g*0.02);

251文字→209文字になりました。
インデントも消せますが、読みづらすぎる状態になってしまうので最後に消すことにします。

小数の記法を修正する

GLSLではint型とfloat型の暗黙的型変換が行われないため、(型変換する際の文字数が勿体ないので)基本的に計算はfloat型で行います。

小数表記は 1.0 → 1.0.1 → .10.0001 → 1e-4 のように短縮形で書くことができます。
また、ベクトルの宣言時にはint型を指定できるため、vec3(p,1.) → vec3(p,1)のように短縮できます。

- vec2 p=(FC.xy*2.0-r)/max(r.x,r.y);
- vec3 e=normalize(vec3(p,1.0));
+ vec2 p=(FC.xy*2.-r)/max(r.x,r.y);
+ vec3 e=normalize(vec3(p,1));
  float g;
  for(int i=0;i<99;i++){
    vec3 q=vec3(e*g);
-   q=mod(q,4.0)-2.0;
-   float d=length(q)-1.0;
-   if(d<0.0001)break;
+   q=mod(q,4.)-2.;
+   float d=length(q)-1.;
+   if(d<1e-4)break;
    g+=d;
  }
- o=vec4(g*0.02);
+ o=vec4(g*.02);

209文字→200文字

ほとんど変わらない部分を省略する

厳密に見ればロジックが変わってしまうけど、見た目にはほぼ影響しない部分があります。

まず1行目の画面座標を正規化する処理では、max(r.x, r.y)として画面の縦横のうち長い方を取得しています。
しかし実際には横長画面で見ることが多い(作るときも横長画面で作る)ので、r.xの方が大きいと決め打ちして max(r.x, r.y) → r.x としてしまいます。

また、8行目の if(d<1e-4)break; はそのピクセルで描画するオブジェクトが見つかり次第(=レイがオブジェクトに当たり次第=距離dが0に限りなく近づき次第)ループを抜ける処理です。
普通に考えればここでループを抜けて当然なのですが...dはその後もほぼ0なので、ループを最後まで回しても出力に使うgにはほぼ影響がありません。
そのため、 if(d<1e-4)break; は丸々消してしまいます。

- vec2 p=(FC.xy*2.-r)/max(r.x,r.y);
+ vec2 p=(FC.xy*2.-r)/r.x;
  vec3 e=normalize(vec3(p,1));
  float g;
  for(int i=0;i<99;i++){
    vec3 q=vec3(e*g);
    q=mod(q,4.)-2.;
    float d=length(q)-1.;
-   if(d<1e-4)break;
    g+=d;
  }
  o=vec4(g*.02);

200文字→172文字まで減りました。

不要な一時変数を消す

たとえば

int a = sin(t);
int b = a * 8;

のようなコードがあったとき、 当然

int b = sin(t) * 8

のようにaを代入してaを使わずに書き換えることができます。
これをフル活用して、変数をまとめていきます。

上から順に代入できる部分を代入していくと...

- vec2 p=(FC.xy*2.-r)/r.x;
- vec3 e=normalize(vec3(p,1));
  float g;
  for(int i=0;i<99;i++){
-   vec3 q=vec3(e*g);
-   q=mod(q,4.)-2.;
-   float d=length(q)-1.;
-   g+=d;
+   g+=length(mod(normalize(vec3((FC.xy*2.-r)/r.x,1))*g,4.)-2.)-1.;
  }
  o=vec4(g*.02);

172文字→114文字に一気に減りました!
可読性のカケラも無くなりましたね。

for文の有効活用

3行目にfor(int i=0;i<99;i++){という記述がありますが、まず初期化式にある=0は省略できるので消してしまいます。

加えて、変化式のi++の部分をi++<99のように終了条件式にまとめてしまうことで、iの1文字分減らすことができます。

  float g;
- for(int i=0;i<99;i++){
+ for(int i;i++<99;){
    g+=length(mod(normalize(vec3((FC.xy*2.-r)/r.y,1))*g,4.)-2.)-1.;
  }
  o=vec4(g*.02);

また、最後の行のo=vec4(g*.02);を、空いた変化式のところに入れてしまいます。
変化式はfor文で毎ステップ実行されますが、oには最終的にfor文の最後の実行の結果が入るので、for文の後に書くのと変わりません。(無駄な代入を98回行うことにはなりますが)
変化式に書くとセミコロンが不要なので、セミコロン1文字分(+今回は改行分も)減らせます。

  float g;
- for(int i=0;i++<99;){
+ for(int i;i++<99;o=vec4(g*.02)){
    g+=length(mod(normalize(vec3((FC.xy*2.-r)/r.y,1))*g,4.)-2.)-1.;
  }
- o=vec4(g*.02);

さらに、for文のiはintではなくfloatでも問題ないため、float型に書き換えます。
int→floatで文字数も増えるし、小数点を書く必要もあるため一見文字数が増えそうですが...
iをfloat型にすることによって、最初に宣言しているfloat型変数gをfor文の初期化式で一緒に宣言することができます。

最終的にはこの形。

- float g;
- for(int i;i++<99;o=vec4(g*.02)){
+ for(float g,i;i++<99.;o=vec4(g*.02)){
    g+=length(mod(normalize(vec3((FC.xy*2.-r)/r.x,1))*g,4.)-2.)-1.;
  }

114文字→105文字に減りました。

インデントを消す&中括弧を消す

for文内のインデントやfor文の中括弧を消して、1行で書いてしまいます。

- for(float g,i;i++<99.;o=vec4(g*.02)){
-   g+=length(mod(normalize(vec3((FC.xy*2.-r)/r.x,1))*g,4.)-2.)-1.;
- }
+ for(float g,i;i++<99.;o=vec4(g*.02))g+=length(mod(normalize(vec3((FC.xy*2.-r)/r.x,1))*g,4.)-2.)-1.;

これによって、105文字→99文字に減りました!

複合代入演算子をまとめる

int a += b;
int c = a;

int c = a += b;

とも書けるので、for文の中身だったg+=...はfor文の変化式部分のgに入れてしまうことができます。
括弧の位置を少し修正して、以下のように書き換えることができます。

- for(float g,i;i++<99.;o=vec4(g*.02))g+=length(mod(normalize(vec3((FC.xy*2.-r)/r.x,1))*g,4.)-2.)-1.;
+ for(float g,i;i++<99.;o=vec4(g+=length(mod(normalize(vec3((FC.xy*2.-r)/r.y,1.))*g,4.)-2.)-1.)*.02);

ということで、98文字で書くことができました!

for(float g,i;i++<99.;o=vec4(g+=length(mod(normalize(vec3((FC.xy*2.-r)/r.y,1.))*g,4.)-2.)-1.)*.02);


さらに短く記述したい

さて、最初451文字で書いていたものを98文字で書くことに成功したわけですが、これはあくまで「最初の描画結果と同じものを維持するという条件の下での最短」です。
「似たような」描画結果でよければもっと文字数を減らせるので、やってみます。

normalizeを消す

「normalize」って文字数多くて鬱陶しいですよね。今回の例では正規化しなくてもそこまで描画結果に差はないので、消してしまいます。

- for(float g,i;i++<99.;o=vec4(g+=length(mod(normalize(vec3((FC.xy*2.-r)/r.y,1.))*g,4.)-2.)-1.)*.02);
+ for(float g,i;i++<99.;o=vec4(g+=length(mod(vec3((FC.xy*2.-r)/r.x,1)*g,4.)-2.)-1.)*.02);

一気に98文字→87文字になりました。

modをfractに置き換える

  • mod(a,b): aをbで割った余りを返す関数
  • fract(a): aの小数部を返す関数

つまり、mod(a,1.)=fract(a)という等式が成り立ちます。
ここで注目したいのが、mod(a,1.)よりもfract(a)の方が1文字少ないという点です。
つまり、もし他のパラメータを調整して、modの第2引数が1.でも似たような描画結果を作れれば、modをfractに置き換えることで1文字減らせるということ。

ごちゃごちゃとパラメータ調整したところ、以下のように数値を修正することで似たような描画結果を得ることができました。(最後の.02の部分が.1になったのでついでに1文字減らせました)

- for(float g,i;i++<99.;o=vec4(g+=length(mod(vec3((FC.xy*2.-r)/r.x,1)*g,4.)-2.)-1.)*.02);
+ for(float g,i;i++<99.;o=vec4(g+=length(mod(vec3((FC.xy*2.-r)/r.x,1)*g,1.)-.5)-.3)*.1);

mod(a,1.)の形になったので、fract(a)に置き換えます。

- for(float g,i;i++<99.;o=vec4(g+=length(mod(vec3((FC.xy*2.-r)/r.x,1)*g,1.)-.5)-.3)*.1);
+ for(float g,i;i++<99.;o=vec4(g+=length(fract(vec3((FC.xy*2.-r)/r.x,1)*g)-.5)-.3)*.1);

87文字→85文字になりました!


元の描画結果

少し変更された描画結果

若干描画結果が変わりましたが、許容範囲としましょう。


さらにさらに短く記述する

これが最短だと思っていたところ、ページ最後にも紹介しているつぶやきGLSLの記事を書かれたのたぐすさん(@notargs)から、もっと減らせるテクニックを教えていただきました。ありがとうございます!

完成形がこちら。

for(int i;i++<99;o+=length(fract(vec4((FC.xy*2.-r)/r.x,1,1)*o)-.5)-.3);o*=.1;

なんと85文字→77文字です。

oを途中計算に利用する

教えていただいたテクニックが、oを途中計算に利用するというもの。
oはgeekest(300 es)Regulationで出力として使えるvec4型の変数です。
そのためvec4 o;のように自分で宣言することなく使うことができ、このoを途中計算にも利用してしまうことで変数宣言分の文字数を減らすというテクニック。(すごい)
oを使うことで、先ほどまで使っていたfloat型の変数gが不要になりました。

oの利用に伴って、vec3をvec4に修正したり、1回だけoに掛けたい.1をfor文の外に出したりしましたが、それでも85文字→80文字まで減りました。

- for(float g,i;i++<99.;o=vec4(g+=length(fract(vec3((FC.xy*2.-r)/r.x,1)*g)-.5)-.3)*.1);
+ for(float i;i++<99.;o+=length(fract(vec4((FC.xy*2.-r)/r.x,1,1)*o)-.5)-.3);o*=.1;

iをintに戻す

float型変数gがなくなったことによって、もうひとつ恩恵がありました。
gと一緒に宣言するためにiをわざわざfloat型にしていたのですが、gがなくなった今、iはint型で問題なくなったので、文字数の少ない&小数点が不要なint型に戻します。

- for(float i;i++<99.;o+=length(fract(vec4((FC.xy*2.-r)/r.x,1,1)*o)-.5)-.3);o*=.1;
+ for(int i;i++<99;o+=length(fract(vec4((FC.xy*2.-r)/r.x,1,1)*o)-.5)-.3);o*=.1;

こうして、最短77文字でフィニッシュです。

for(int i;i++<99;o+=length(fract(vec4((FC.xy*2.-r)/r.x,1,1)*o)-.5)-.3);o*=.1;

twigl.appのgeekest(300 es)Regulationにコピペしてみても、想定通りに描画されています。


さいごに

GLSLはハードルが高い印象があったのですが、ここ2〜3週間触っているうちにどんどん楽しくなってきました。
つぶやきGLSLに夢中になりすぎて、業務で可読性最悪のコードを書かないように気をつけます🤪

最後に、つぶやきGLSLのおすすめ文献と、自分が作ったつぶやきGLSLをいくつか載せておきます!

つぶやきGLSLのおすすめ文献

https://www.slideshare.net/yutakasato391/glsl-249579645
つぶやきGLSLのやり方、考え方について、入門者向けに分かりやすく解説されています。


https://notargs.hateblo.jp/entry/twigl_minify
文字数少なくGLSLを書くテクニックが紹介されています。


つぶやきGLSL作例

https://twitter.com/yuji_ap/status/1472189864590589959
https://twitter.com/yuji_ap/status/1469662126646398977
https://twitter.com/yuji_ap/status/1470011879242473487

Discussion