Tokyo Demo Fest 2021のGLSL Graphics Compo優勝作品の解説

これはKLab Engineer Advent Calendar 2021の20日目の記事です。


12月11日~12日にオンラインで開催されたTokyo Demo Fest 2021(以下、TDF)に参加しました。

TDFは、日本国内で唯一のデモパーティです。 リアルタイムに映像や音楽を生成するプログラムを「デモ」と言い、デモを鑑賞したり完成度を競ったりして楽しむイベントを「デモパーティ」と言います。 「デモシーン」はデモやデモパーティを中心としたコンピューターのサブカルチャーです。

今年のTDFでは、『Alien Spaceship』という作品を発表しました。

TDFのGLSL Graphics Compoにて、本作品が1位に選ばれました!

この記事では『Alien Spaceship』の利用技術と制作の裏側について解説します。

GLSL Graphics Compoとは?

デモシーンの文化に馴染みのない方に向けて、簡単にGLSL Graphics Compoの概要や制約について説明します。

GLSL sandboxはWeb上でGLSLのフラグメントシェーダーを編集・実行できるWebGLで実装されたサービスです。作品を公開したり共有もできます。

GLSL Graphics CompoはGLSL Sandbox上で動作するGLSLのシェーダーによるグラフィックスを競うコンポです。 コンポはコンペティションの意味で、参加者投票によって順位が決まります。

GLSLシェーダーだけで映像をつくる

そもそもGLSLシェーダー、つまり プログラミングのソースコードだけで映像をつくる 行程を一般的には想像しづらいかもしれません。

まずは次の図を見ていただけると、具体的にイメージを掴めるかもしれません。 GLSLのコードからコメントや改行・空白文字を取り除き、処理の内容で色分けしました。

GLSLのコードの処理

この7756文字のGLSLのシェーダーに映像のすべてが実装されています。

見てのとおり シーンのモデリング、ライティング、カメラワーク、演出のシーケンスがすべて含まれています。

変数名や関数名を1文字に短縮したり、デバッグ用のコードの削除はしていないので、まだまだ文字数を削る余地はあります。 今回は文字数をそこまで意識してコーディングせずに、可読性を重視しました。

GLSL sandboxでは音楽を再生できないので、YouTubeの音楽は後付けです。Shadertoy標準楽曲「Most Geometric Person」を使わせていただきました。

レイマーチング

GLSL sandbox用のGLSLのフラグメントシェーダーで記述できるのは、フルスクリーンのMeshを描画する2D処理のみです。

入力は描画対象のピクセルの座標、出力はピクセルの画素値の単純な2D処理です。 また、時間やマウス座標を入力にすることで、アニメーションもできます。

3Dを描画するためには、GLSLコードの中に3Dのカメラや3Dのシーンの形状を定義する必要があります。

2DのGLSLのシェーダーで3D空間を描画するためのテクニックとして、レイマーチングがよく使われます。

レイマーチングは、距離関数の長さだけひたすらレイを進める処理をくり返し、距離関数が0になったら衝突したと判定する単純なアルゴリズムです。 つまり、レイトレーシングの交差判定のアルゴリズムのひとつです。 レイマーチングは、描画する形状を距離関数という数式によってプロシージャルに定義できるため、3Dのモデリングなしに3Dシーンを描画できます。

レイマーチングの詳細については、過去に勉強会のスライドや書籍で紹介しています。

Alien Spaceshipの技術解説

前置きが長くなりましたが、ここからレイマーチング経験者に向けた技術解説をします。

技術的なポイントとしては次の3点です。

  • 宇宙船の船内のような具体的な対象を目指したモデリング
    • SDF(距離関数)によるモデリングでは少し難しい
  • リアルタイムなグローバルイルミネーション(GI)あるライティング
    • 事前計算なしのGIのリアルタイム計算は技術的にとても難しい課題
  • 長尺のタイムラインのシーケンス
    • シェーダーはカメラワークや演出のシーケンスの実装に適した道具ではないが、なるべくスマートな実装になるように工夫

モデリング

全編を通してプリミティブとしてはBoxとSphere(卵)の2種類しか使っていません。

前半のHallwayシーン

壁の光る部分はBoxをSkewしたり、床はBoxにDisplacement Mapでディテールを加えています。

party1164.jpg

壁のSkew

壁の の字の折り曲がった形状には、BoxをSkewで変形させています。

p.x -= W - 0.5 * abs(p.y);// Skewで変形
opUnion(m, sdBox(p, vec3(a * 1.7, H, 0.24)), SOL, roughness, 0.0);

床のDisplacement Map

床のDisplacement Mapは次のような数式で実装しています。

// hをsdBoxの第2引数のサイズに加算すると、Displacement Mapになる
float h = 0.1 * floor(2. * sin(p.x)) + 0.2 * floor(sin(2. * p.z));

sinから滑らかなカーブを得て、それをfloorで階段状に離散化しているだけです。

pは事前にabs(p.x)により左右ミラーしています。

床の断面

扉の台形波

扉の台形のギザギザの関数はkaneta先生のコードをお借りしました。

float smoothPulse(float start, float end, float period, float smoothness, float t) {
    float h = abs(end - start) * 0.5;
    t = mod(t, period);
    return smoothstep(start, start + h * smoothness, t) - smoothstep(end - h * smoothness, end, t);
}

float y(float x) {
    return smoothPulse(0.0, 0.6, 1.0, 0.5, x);
}

扉の台形波

床のEmissiveや扉を開けたときのEmissiveの模様のパターンもsmoothPulse関数を用いました。

party2085.jpg

smoothPulsePattern.png

// Floor Emissive Pattern
float py = smoothPulse(0.0, 0.6, 1.0, 0.5, 0.25 * p.y);
float emi = smoothPulse(0.2, 0.25, 1.0, 0.5, py + p.x / 2.0);

Shadertoyに簡単なサンプルを用意しました。

Hallwayシーンまとめ

天井についても、係数を調整しながら箱を並べることで、狙った形状をモデリングしていきました。 特殊なことは何もしていませんが、sdBoxの評価回数が増えると負荷が高くなるので、なるべくsdBoxの数を減らすように意識しました。 レイマーチングでは、座標をmodで繰り返すと特定の軸に対して無限にオブジェクトを配置できます(opRep)。 前述の左右のミラー化もsdBoxの評価回数を減らすための工夫のひとつです。

ほぼopRepとSkewとDisplacement Mapのテクニックの繰り返しで地道にモデリングしているだけです。

ライティングの問題とモデリングの問題を切り分けるためにシンプルなレイマーチングの描画モードも用意しました。

よく見ると強引にSkewとDisplacement Mapをしたために、よく見るとアーティファクトが発生しています。 最終的なライティングでは暗い箇所となってほとんど目立たなかったので、今回はそのままにしました。

debug-scene.png

後半のAlienの巣のシーン

IFS(Iterated Function Systems)をつかっています。

party6370.jpg party7186.jpg

IFSでは狙った形をモデリングすることは困難なので、パラメーターを延々と調整しながら、理想的な見た目になるまで試行錯誤を繰り返しました。

// IFSのパラメーター
vec4 ifs = vec4(875, 482, 197, 545) / vec2(1200, 675).xyxy;

// IFSでモデリング
p = pos;
p -= vec3(0, H, 16. * 3.5);

for (int i = 0; i < 5; i++) {
    p = abs(p) - ifs.w;
    rot(p.xz, -4. * ifs.x);
    p = abs(p) - ifs.z;
    rot(p.xy, -4. * ifs.y);
}

opUnion(m, sdEgg(p, 0.1), SOL, 0.0, 0.0);
opUnion(m, sdBox(p, vec2(1, 0.01).xyy), SOL, roughness, 0.0);
opUnion(m, sdBox(p - vec2(0.001, 0).yxy, vec2(1, 0.01).xyy), VOL, 2.4 * saturate(cos(beatTau / 2. + 10. * p.x)), 2.4);

ライティング(グローバルイルミネーション)

party1895.jpg

全編を通してグローバルイルミネーション(GI)や、少しラフな反射(roughness = 0.05くらい)をしています。

グラフィックスエンジニアなら性癖に刺さるポイントだと思います。

GIをリアルタイムに計算するのは技術的にはとても難しい課題です。

今回はVirgillさんが開発したMadtracingを用いてGIを計算しました。

MadtracingはEnd of time by Alcatraz & Altairというデモで使われた手法です。

Madtracing解説用のシェーダーがShadertoyに公開されています。

パストレーシングと同じように表面のroughnessに応じてセカンダリレイを飛ばしてGIを計算します。

通常のパストレーシングでは物体の表面にヒットしてからセカンダリレイを複数回飛ばすと思いますが、 Madtracingではレイマーチングのステップ中にセカンダリレイを近傍のオブジェクトのroughnessに応じて飛ばします。

これによってボリューム感やBloom感のあるライティングを実現できます。その代償として、少々負荷が高い印象です。

今回のデモでは、Madtracingを自分の使いやすい形に少しだけフォークして利用しました。

まず、マテリアルのフォーマット(map関数の返り値)を以下のように定義しました。

vec4 m = vec4(1, VOL, 0, 0);
// x: Distance
// y: MaterialType (VOL or SOL)
// z: Roughness in (0-1), Emissive when z>1
// w: ColorPalette

MadtracingからAA処理を削除して、AA処理はプライマリレイの生成に移動しました。これで少し負荷削減とシンプル化ができました。

// Ref. EOT - Grid scene by Virgill
// https://www.shadertoy.com/view/Xt3cWS
void madtracer(vec3 ro1, vec3 rd1, float seed) {
    scol = vec3(0);
    float t = 0., t2 = 0.;
    vec4 m1, m2;
    vec3 rd2, ro2, nor2;
    for (int i = 0; i < 160; i++) {
        m1 = map(ro1 + rd1 * t);
        // t += m1.y == VOL ? 0.25 * abs(m1.x) + 0.0008 : 0.25 * m1.x;
        t += 0.25 * mix(abs(m1.x) + 0.0032, m1.x, m1.y);
        ro2 = ro1 + rd1 * t;
        nor2 = normal(ro2);
        rd2 = mix(reflect(rd1, nor2), hashHs(nor2, vec3(seed, i, iTime)), saturate(m1.z));
        m2 = map(ro2 + rd2 * t2);
        // t2 += m2.y == VOL ? 0.25 * abs(m2.x) : 0.25 * m2.x;
        t2 += 0.25 * mix(abs(m2.x), m2.x, m2.y);
        scol += .007 * (pal(m2) * step(1., m2.z) + pal(m1) * step(1., m1.z));

        // force disable unroll for WebGL 1.0
        if (t < -1.) break;
    }
}

「絶対に実行されないbreak」によるコンパイル時間削減

madtracer関数に、謎の if (t < -1.) break; があることに気がついたでしょうか?

tはレイの進んだ距離で、絶対にマイナス値にはなりません。つまり絶対に実行されないbreak処理です。 普通に考えれば不要な処理ですが、これはGLSLコンパイル時間削減のハックです。

breakを追加することで、GLSLコンパイラによってforがunrollされずにloopとして処理されて、コンパイル時間を大きく削減できます。

ChromeデフォルトのWebGLのANGLE有効時にはかなり効果的で、自分の環境ではコンパイル時間を32.9秒から1.7秒に削減できました。

コンポ提出当日はずっとコンパイル時間の削減に工数を費やしていて、提出2.5時間前くらいに気がついたので、もっと早く気がついていればという気持ちです。

同様のテクニックとして、N + min(0, iFrame) をループ回数にする手法があります。Danilさんに教えていただきました。

コードにすると、こういう感じです。

for(int i = 0; i < 160 + min(0, iFrame); i++) {
    // ループ中の処理
    // ...
}

ShadertoyなどのWebGL2.0環境であれば、この方法で同じコンパイル時間削減の効果を得られます。

WebGL1.0の場合はダイナミックループをサポートしていないので、WebGL1.0で動くGLSLSandboxでは N + min(0, iFrame) のハックは使えません。

GLSLSandbox用なら、絶対に実行されないbreak のハックを使うと良いでしょう。

タイムラインのシーケンス

タイムラインのシーケンス管理のために次の簡単なマクロを実装しました。

// Timeline
float prevEndTime = 0., t = 0.;
#define TL(beat, end) if (t = beat - prevEndTime, beat < (prevEndTime = end))

使い方は簡単で、TLの引数に現在時刻と境界値(区間の終了タイミング)を指定します。 単位は区別していないので、時間単位でもビート単にでも統一されていてばOKです。

グローバル変数tに現在区間の相対的な時間が自動的に設定されるため、処理をスッキリと書けます。

ifの条件の中にカンマを複数の式を書けるのは今回はじめて知りました。

// カメラワーク制御の実装例

// 0~ 4*8ビート目までの処理
TL(beat, 4. * 8.) setCamera(vec4(600, 250. + t * 3., 600, 243. - t * 6.), 3.);

// 4*8~4*10ビート目までの処理
else TL(beat, 4. * 10.) setCamera(vec4(600, 307, 600, 44. + t * 4.), 3.);

// 4*10~4*12ビート目までの処理
else TL(beat, 4. * 12.) setCamera(vec4(494, 322, 695, 216), 2.4 + 0.2 * t);

// 4*12~4*14ビート目までの処理
else TL(beat, 4. * 14.) setCamera(vec4(600, 481. + 10. * t, 600, 59), 3.);

今回はカットごとにカメラを完全に切り替えていたので、このような仕組みでうまくカメラワークを実装できました。

おわりに

kanetaさんのsmoothPulse関数や、VirgillさんのMadtracing以外にも、数え切れないほどたくさんの解説記事とシェーダーを参考にしたり、たくさんの作品に影響を受けました。 たくさんの方々に感謝します。ありがとうございました!

感想

ここからは技術的なこと以外のポエムをつらつらと書きます。

GLSL Graphics Compo初優勝!

2018年のPC Demo Compoに引き続き、Tokyo Demo Festでのコンポ優勝は2回目です。

これまでGLSL Graphics Compoはずっと3位で、なかなか優勝できなかったので、ようやく心残りを解消できました。

トロフィーの素材や厚みが例年よりも高級感があって、個人的にもなんだか嬉しい気持ちです(笑)。

GUNCY’Sさんによる副賞のRazer BlackWidow V3 Green Switchもありがとうございます。

気軽にTDFにエントリーしてほしい

GLSL Graphics CompoはTDF独自のコンポで、海外のパーティでは見たことのない形式ですが、個人的にはとても好きです。

2016年のTDFに初参加したとき、一晩でGLSLSandboxのシェーダーを書いて、GLSL Graphics Compoにエントリーした記憶は今でも鮮明に覚えています。 自分のシェーダーが巨大なスクリーンに映し出されたとき、オーディエンスの歓声が聞こえて本当に嬉しかったです。 この体験がなければデモシーンやシェーダーを続けていないような気がします。勇気を出してエントリーして良かったと本当に思います。

デモを1本完成させるのは本当に大変ですが、GLSL Graphics Compoなら気軽に参加できることがメリットだと思います。

気軽に参加できる数少ないコンポですが、近年のGLSL Graphics Compoのレベルはインフレを続けて、上位勢はかなりガチな作品を出してくるなという印象があります。

本来のGLSL Graphics Compoは数秒から10秒程度の短いグラフィックス作品の部門だと自分は認識しています。 Traveler 2やAlien Spaceshipのような長尺のデモっぽい作品がGLSL Graphics Compoに増えることで、もし他の参加者が萎縮してしまったらとても不本意な気持ちです。

GLSL Graphics Compoは順位や周りを気にせず、1晩クオリティの雑なシェーダーでも構わず気軽にエントリーできる雰囲気にして、新規参入者が増える未来を望んでいます。

オンラインパーティの体験

今回のTDF初のオンライン開催でした。

TDFのオーガナイザーの方々の努力のおかげで、実際のデモパーティにかなり近い体験を再現できていたのではないかと思います。

Day2のYouTubeの視聴回数が3000回を超えているので、例年のオフラインパーティよりもたくさんの人に見てもらえたなど、オンラインのメリットも感じました。

ですが、やはり正直に言うと「オンラインだと物足りないなぁ…」というのが正直な感想でした。 とくにオーディエンスの反応や会場の熱気を直接感じられないのはとても寂しかったです。またオフラインでデモパーティできる日が本当に待ち遠しいです。

Shader Showdown

TDF初の試みであるShader Showdownは本当に激熱でした。

とくに決勝戦の phi16 vs. Kamoshika の戦いは一生忘れないくらい印象に残りました。

Shader Showdownについては、別の記事に書きました(12/31)。

おわりに

さいごに、関係者のみなさんに感謝を申し上げます。 TDFのオーガナイザーの方々、エントリーしてくださったみなさん、YouTubeで視聴してくださったみなさん、応援してくださった方々、ありがとうございました!

その他

本編では言及しなかったけれども一応書いておきたいことを箇条書きでつらつら書きます。

  • 今年のTDFでは、KLabはゴールドスポンサーとして協賛
    • 協賛できて良かった
  • なぜGLSL Graphics Compoに出したの?
    • 音楽を作る能力と余裕があれば、IntroとしてPC Demo Compoに出したかったが、間に合わなかった
    • sadakkeyさん多忙
    • 来年は音楽も勉強したい(毎年言っている気もする)
  • 計画的にデモを作りたい
    • TDF直後には、他の人や作品に感化されて、溢れるモチベーションとやる気があるはずなのに
    • 結局毎年締切ギリギリまで着手できない
    • だんだん徹夜もつらくなってきた
  • 着想点
    • グローバルイルミネーションをやりたかった
    • Area Lights with LTCsも調査はした
      • BRDFなどに依存したルックアップテーブルが必要で、1Pass実装にフォールバックが不可能っぽいので諦めた

作業日記

ネタ供養🙏です。

2021-11-07

2021-11-07-v1-1.png

2021-11-16

2021-11-16-v1-1.png

2021-11-16-v1-2.png

2021-11-16-v1-3.png

この頃はIFSを弄っていた。

2021-11-17

2021-11-17-v1-1.png

2021-11-17-v1-2.png

2021-11-18

2021-11-18-v1-1.png

2021-11-18-v2-1.png

2021-11-19

2021-11-19-v1-1.png

ボロノイでザラザラとした床にする案

2021-11-19-v1-2.png

2021-11-19-v1-3.png

2021-11-19-v1-4.png

2021-11-20

2021-11-20-v1-1.png

2021-11-21

2021-11-21-v1-1.png

2021-11-21-v1-2.png

2021-11-21-v2-1.png

2021-11-22

2021-11-22-v1-1.png

2021-11-22-v2-1.png

2021-12-02

2021-12-02-v1-1.png

2021-12-02-v2-1.png

2021-12-03

締切当日はコンパイル時間の削減をがんばっていた。

comments powered by Disqus

gam0022.net's Tag Cloud