Tokyo Demo Fest 2018のGLSL Graphics Compoで優勝した!作品の解説等
2018年の12/1と12/2の2日間に渡って、Tokyo Demo Fest 2018という日本唯一のデモパーティが開催され、そこに初参加してきました。
僕はそのイベントの、GLSL Graphics CompoとCombined Graphics Compoに作品をエントリーして1位と4位に選んで頂きました!ありがとうございます!
#TokyoDemoFest 2018 GLSL Compo 1位の作品をShaderToyに投稿しました!
— かねた (@kanetaaaaa) 2018年12月2日
本日はありがとうございました!!https://t.co/5SFfmtkw86 pic.twitter.com/Xu8QZk0Rpw
GLSL Compoネタでお蔵入りになっていたものを、急遽Graphics Compo用にポートしたやつ置いておきますhttps://t.co/xK8hVyMpHN#TokyoDemoFest #tdf2018 pic.twitter.com/3k27iNYZji
— かねた (@kanetaaaaa) 2018年12月1日
GLSL Graphics Compo作品の動画やソースは以下から見ることができます。
Graphics Compoの作品は当日会場に到着してから、友人と話しているときに思いついて、GLSL Graphics Compo用のネタでお蔵入りしていたシェーダーを流用して、iq氏の4kテンプレートで大急ぎで作りました。
僕がGLSL Graphics CompoにエントリーしたTraveler 2は、偉大な先人の方々の解説記事や作品に多大な影響を受けています。誰の作品にどんな影響を受けたかはこちらの記事に全てまとめているのでご参照ください。 qiita.com
この記事では、先人達への感謝の気持ちを込めて、さらに深堀して作品の解説をしたいと思います。
Traveler 2の解説
コンセプト
過去の入賞作品や、TDF2017のGLSL Graphics CompoのYoutube動画を何度も見て、以下の条件に当てはまるものを作ろうと考えました。
- 過去の作品にはないような見た目
- 単純に新しい体験の方が楽しめる
- 過去の作品に比べて尺が長め
- 上映時間が長い方が印象に残るかも?
- ほかの作品が大体30秒ほどなので、差別化できるかもしれない
- あまり長くすると退屈するかもしれないので2分を目安
- 上映中に退屈になる時間が無い
- GLSL作品は任意の音楽を再生できないので、常に画面に変化を加える
- Traveler 2では大体8beat毎に新しい変化を加えた
- 小気味のよい動き
- 2017年優勝作品がぬるぬる動いていた
結果、「1パスGLSLだけで作られているとは思えないようなメガデモ風の作品」というコンセプトの元にTraveler 2が誕生しました。
「これが1パスGLSLだけで作られてるの!?」と一人にでも思っていただけていればうれしいです。
時系列
今年の6月からレイマーチングの勉強を初めると同時に、Traveler 2の元となるtraveler.という作品を2週間で作りました。
グラフィックの知識が多少あったので、割とすんなりレイマーチングを習得することができました。
その後10月の下旬までの4ヵ月間、シェーダー芸のポキャブラリーを増やすために、Shadertoyや既存の作品のコードを読んだり、小ネタを実装したりしていました。
10月下旬からTDF本番までは、できるようになったことをtraveler.に詰め込んでTraveler 2が完成しました。
traveler.からTraveler 2までの履歴はすべてこちらのPRに残っているので、興味がある方はご覧ください! github.com
ロード画面について
当日の上映時は緊張で回りの音が聞こえてなかったんですけど、アーカイブを見ると、最初のロード画面に困惑されている方が多そうでした。
皆さんお察しの通りだと思いますが、GLSL Sandboxでは事前にロードする処理等は書けないので、実際にロードしているわけではありません。
ではなぜ、30秒もロード画面を表示したのかといいますと、シェーダーのコンパイルに20秒近く掛かってしまうからなんです。
GLSL Sandboxはコンパイル中も時間が進み続けてしまい、20秒もコンパイルをしていると、作品が途中から再生されてしまうという可能性があったんです。
なので最初の30秒間に待機画面を用意して、必ず作品を最初から再生してもらう、というのが狙いでした。
しかし上映時はブラウザに、リハーサルした際のキャッシュが残っていたのか一瞬でコンパイルが完了していましたね...
皆さんには30秒待ってもらうことになってしまいましたが、逆にインパクトがあったのかなぁとも思います。
背景
背景の3Dオブジェクトは、すべてメンガーのスポンジを改造したものを2つ重ねて作りました。
序盤のシーンでは重ねたもののうち、一つをbeatに合わせてIFS中のイテレーション毎に回転させています。
後半は、追加でz軸を中心に回転foldをして複雑な形状を作りました。
ポストエフェクト
今回は描画結果に対して3種類のポストエフェクトを使用しました。
グレアエフェクト
中央の球体と進行方向にグレアエフェクトを使用しました。
といっても1パスなので、よく使われるような描画結果をボカして合成するといった手法は使えないので、レイの進行方向とオブジェクトの角度を光量とすることにしました。
float flare = pow(max(0.0, dot(vec3(0.0, 0.0, 1.0), ray)), stageFlareExp * 1.25); float flare2 = pow(max(0.0, dot(vec3(0.0, 0.0, 1.0), ray)), stageFlareExp); vec3 f = flare * stageFlareCol + flare2 * di * stageFlareCol * 0.05; float sflare = pow(max(0.0, dot(normalize(sp - ro), ray)), travelerFlareExp * 1.25); float sflare2 = pow(max(0.0, dot(normalize(sp - ro), ray)), travelerFlareExp); vec3 s = sflare * travelerFlareCol + sflare2 * di * travelerFlareCol * 0.05;
かなりいい加減な方法ですが、割とよい雰囲気を出せたように思います。
レンズダート
レンズに埃のようなものが付いているときにでるあれです。
バトルフィールドか何かのポストエフェクトで見て、かっこよかったので入れてみました。
実装はとても単純で、画面をいくつかのセルに分けてそれぞれに、ダートエフェクトを表示します。
その時にセル毎の乱数でダートエフェクトの位置を変えることで、不規則に並べることができます。
それだけでは、密度が足りないのでいくつかのレイヤーに分けてリアルタイムでダートマスクを生成しました。
vec3 dirt(vec2 uv, float n) { vec2 p = fract(uv * n); vec2 st = (floor(uv * n) + 0.5) / n; vec2 rnd = hash(st); float c = Bokeh(p, vec2(0.5, 0.5) + vec2(0.3) * rnd, 0.2, abs(rnd.y * 0.4) + 0.3, 0.25 + rnd.x * rnd.y * 0.2); return vec3(c) * exp(rnd.x * 4.0); } vec3 di = dirt(uv, 3.5); di += dirt(uv - vec2(0.17), 3.0); di += dirt(uv- vec2(0.41), 2.75); di += dirt(uv- vec2(0.3), 2.5); di += dirt(uv - vec2(0.47), 3.5); di += dirt(uv- vec2(0.21), 4.0); di += dirt(uv- vec2(0.6), 4.5);
ダートマスクのみを切り出したものをこちらに用意しているのでご参照ください。
周辺減光
本来はレンズに発生する光学現象ですが、今回はいい感じに画面端を暗くすることでそれっぽい雰囲気を出しました。
お手軽に画面のクオリティアップを狙えるのでオススメです。
vec2 uv = fragCoord.xy / iResolution.xy; uv *= 1.0 - uv.yx; float vig = uv.x*uv.y * 200.0; vig = pow(vig, 0.1); col = saturate(pow(col, vec3(1.0 / 2.2))) * vig;
疑似パーティクル
途中のシーンから表示されるパーティクルのようなものは、ポストエフェクトで紹介したレンズダートとほぼ同じことをしています。
レンズダートは2Dでしたが、こちらは3D空間でmodして増やした球やボックスをセル毎に乱数で間引く + 中央から乱数で少し位置を変えるということをした上で、全体を真上にスクロールしました。
また球は、セルの中央を起点にランダムに回転させることで複雑な動きをしているように見せかけています。
疑似パーティクルの部分のみを切り出したものをこちらに用意したのでご参照ください。
2DのIFSで模様
球体や背景の模様はとても単純で、2DのIFSを使用してボックスの輪郭のみを描画しています。
実際に使用しているコードがこちらにありますが、とても簡潔ですね。
vec3 tex(vec2 p, float z) { vec2 q = (fract(p / 10.0) - 0.5) * 10.0; float d = 9999.0; for (int i = 0; i < 5; ++i) { q = abs(q) - 0.5; q *= rot(0.785398); q = abs(q) - 0.5; q *= rot(z * 0.5); float k = sdRect(q, vec2(1.0, 0.55 + q.x)); d = min(d, k); } float f = 1.0 / (1.0 + abs(d)); return vec3(smoothstep(0.8, 0.9, f)); }
ボックスを軸で折り畳みして平行移動・回転するという動作を繰り返すことで複雑な模様が生まれます。
この時引数のzを変えることでいろいろなバリエーションの模様ができるのですが、Traveler 2の模様を作るにあたって、パラメータをtimeにして眺めながら、好みの模様ができるtimeをメモしてコードに直打ちしました。
模様部分のみを切り出したものを以下に用意していますのでご参照ください。
モーションブラー
モーションブラーを愚直に実装してしまうと、1フレーム内で過去のレンダリング結果をいくつか計算しなおしてブレンドするという方法になります。
Traveler 2も最初はそうしていたんですが、後半になるにつれて処理負荷が無視できないものになってきました...
そこで、最終的に採用したのはピクセル毎に時間をずらすという単純な手法です。
多少見た目がノイジーになってしまいましたが、フルHDで再生する分には許容範囲でした。
実装はこのようになっています。
beat = (t + hash(p).x * 0.0065 * (1.0 - saturate((orgBeat - 230.0) / 4.0)) * step(12., orgBeat)) * BPM / 60.0;
tはtimeです、 * (1.0 - saturate((orgBeat - 230.0) / 4.0)) * step(12., orgBeat))
こんなよく分からないものが付いていますが、これは、最初と最後にカメラが大きく揺れるシーンがあり、その二か所のみノイズが許容できなかったのでモーションブラーをしないようにする処理です。
カメラの手振れ
これはかなりオススメなんですが、カメラの手振れにfbm(fractal brownian motion)を使用しました。
最初はカメラの位置と視点を揺らすつもりだったんですが、元々結構ギリギリなカメラワークをしていたりしたので、一部のシーンでカメラがめり込んでしまいました...
Traveler 2ではカメラをめり込ませたくなかったので、レイを飛ばす前にスクリーン座標をfbmでオフセットすることで、手振れさせても絶対にめり込まないようにしました。
vec2 pp = p + (vec2(fbm(vec2(beat * 0.1), 1.0), fbm(vec2(beat * 0.1 + 114.514), 1.0)) * 2.0 - 1.0) * .65; vec3 col = scene(pp) * glitchColor;
カメラに手振れを追加すると無機質な映像が、一気に臨場感ある映像になって感動しました!
苦労話
コンパイルに時間がかかりすぎてChromeがクラッシュする...
最終提出時に手元のPC(i7 2.7GHz/GTX1050)ではコンパイル時間18秒ぐらいで、コンパイル時に6回に1回ぐらいクラッシュする状態でした...
最強のコンポマシンならきっと動く!と神に祈る勢いで提出したんですが、ちゃんと上映されてホッとしています。
コンパイル時間の問題はWindows限定で、おそらくAngleを経由してOpenGLを動かしているからだと推測していますが、正しいことはわかりません。
ちなみにChromeのuse-angleオプションを使用してOpenGLを直接動かすと、Windowsでもコンパイルが爆速になります。詳しい話は以下の記事を見てください。
シェーダーがクラッシュして、もう機能追加できない!という状態が制作中に3度ほどありましたが、品質を落としたり、ifを撲滅したりすることで何とか納得のいく状態まで作り終えることができました。
コンパイル時間改善にもっとも効果があったのがifの撲滅で、以下のPRを適用すると、20秒だったコンパイル時間が12秒になりました。
まだまだTraveler 2に入れたいものがあった...
コンパイル時間の問題が無ければ追加したいものがまだまだありました...
事前に用意していて入らなかったものを2つ紹介します。
タイトルアニメーション
一番最初に球体が飛んでいくシーンの直前に入れようとこんなものを用意していました。
残念ながらコンパイル時間が長すぎて入れることができませんでした...
TDFロゴ
Graphics Compoに提出したGLSL Compoでお蔵入りになったと言った奴です。
これは3Dの距離関数として作っていて、Traveler 2序盤の不自然なほど激しいカメラワークが2箇所あるのですが、そこで一瞬TDFの3Dロゴを表示しようと考えていました。
作品の最終シーンがなかなか決まらない...
提出版最終シーンは、紆余曲折あって、画面に徐々にノイズが現れて、最後にシャットダウンするようなシーンになっています。
最初は別プランでやっていましたが、終わり感が出せずいまいちしっくりこなかったため、同僚の@amanatsu_nit氏に相談したところ、シャットダウン演出を提案してもらい、モックまで作ってもらいました!
ここにたどり着くまでになかなか迷走しましたが、本番の前日ぐらいまでモックを見ながら、良しなに調整できたので、盛り上がる最終シーンに仕上がったと思います。
さいごに
Tokyo Demo Festは、同僚のgam0022氏が2016年のGLSL Graphics Compoで3位入賞して、そこで初めて存在を知りました。
同僚の活躍を見て、僕も作品を出したい!と常々思っていたのですが、時間があまりとれずに、今年の開催が12月にずれたこともあって、ようやく初参加することができました!
僕はコミュニケーションが苦手なので知人以外とあまり喋ることができませんでしたが、後半はtwitterでいつも見かけるような有名人の方とも沢山お話しできてとても楽しかったです。
特に印象に残っているのは、2nd Stage Boss等の4k作品を手掛けたよっしんさんが「この量のコードなら4kに収まる」と仰っていたことで、驚きすぎて耳を疑いました...(25000charsもあるのに...)
個人的に話してみたいなぁと思っていた方も見かけたのですが、声を掛けることができなかったので、来年は勇気を出して声を掛けたいと思います!
今後は4kbに収まるようにコードを書く技術や、音楽を作る練習をして、次回作品をエントリーするときは4k作品をエントリーしたいです!(作品が間に合わなくても、来年もTDFを見に行きます!)
最後になりましたが、Tokyo Demo Fest 関係者の皆様、最高のイベントありがとうございました!!
おまけ
誰も見ていないだろうと、最初からパブリックなリポジトリでTraveler 2を作っていたんですが。
突然レイトレ若人にマサカリを投げられてしまいました...
心が折れたので、来年からはプライベートリポジトリで作業しようと思います!