2 ポイント 投稿者 GN⁺ 2025-02-08 | 1件のコメント | WhatsAppで共有
  • 240行のGLSLコードだけでRickのアニメーションを作り、画像やライブラリなしでGPUシェーダーが各ピクセルの色を時間に応じて計算する
  • 核心は color_for_pixel(pixel, time) がすべてのピクセルで実行され、符号付き距離場(SDF) が図形の内外と輪郭を距離値で表現する構造にある
  • 顔は round_rect(), circle(), star(), bezier(), parabola() のようなSDF図形を移動、スケール、ミラーリング、反復、和集合で組み合わせて構成する
  • 髪のうねり、歯の反復、唇の輪郭、まばたき、瞳の移動、首振りは ドメインワーピングmod(), sin(time), noise() によって段階的に加えられる
  • 完成版にはポータル背景まで含まれるが、動画書き出しはエディタにないため、glslviewerffmpeg を使う別途のmacOSワークフローが必要になる

ピクセルごとの色を計算するシェーダー構造

  • アニメーションは OpenGL Shading Language(GLSL) で記述されている
  • ページ内の ライブコーディングエディタ でサンプルをそのまま実行・修正できる
  • 基本のエントリーポイントは color_for_pixel(vec2 pixel, float time) 関数
    • GPUがプレビュー内のすべてのピクセルに対してこの関数を実行する
    • 関数が答える問いは「この時刻にこのピクセルはどんな色であるべきか」
  • 簡単なサンプルでは pixel.x, pixel.y, time の値をカラーチャンネルに入れ、ピクセル位置と時間変化を可視化する
  • time は最後の編集以降に経過した秒数で、継続的に増加する

SDFで図形を描く方法

  • 円は length(pixel) で原点からピクセルまでの距離を求め、それを半径と比較して描ける
  • circle() 関数は単に内外を示す bool ではなく、周縁までの距離を返す
    • 図形の内側は負
    • 図形の外側は正
    • 境界は0に近い値
  • このような関数が 符号付き距離場(SDF) 関数
  • 距離値を使えば塗りと輪郭線を同じやり方で扱える
    • dist < -0.01 なら内部色
    • dist < 0.0 なら黒い輪郭線
  • 複数のSDF図形は min() で結合できる
    • Rickの頭と耳を別々に輪郭処理すると、その間に不要な線が出る
    • 2つの距離値を min() でまとめると、頭と耳の 和集合の輪郭 だけが残る
  • ほかの結合方法もあり、参考資料としてInigo Quilezの 2D distance functions と primitive combination 資料がリンクされている

Rickの顔を構成する図形たち

  • 頭の形は round_rect() で作り、耳も別の round_rect() で追加する
  • シーズン1ポスターのRick画像をプレビュー上で点滅表示しながら重ね、数値を合わせていく
    • 多くの数値はこの試行錯誤の過程で見つけたもの
    • 色の値は画像編集ソフトのスポイトツールで取得している
  • 目は円形SDFと6ポイントの star() で構成される
    • 眼球は pixel.y *= .93 のような座標変形で縦方向にわずかに引き伸ばす
    • 瞳は6ポイントの星形SDFから小さな距離値を引いて角を丸める
  • 2つの目はコードを複製せず、pixel.x = abs(pixel.x) による 左右ミラーリング で作る
  • 鼻、口、眉は bezier() で描く
  • 髪は縦に引き伸ばした 11ポイントの star() から出発する
  • 図形を描くコードの順序も結果に影響する
    • 先に返される色が前面に見える図形になる
    • 歯と舌は口の形の内部条件分岐の中でのみ描かれるため、口の外には出ない

髪、歯、装飾線を整えるテクニック

  • 硬い星形の髪は ドメインワーピング(domain warping) で波のように変わる
    • ドメインワーピングはピクセル位置に基づくランダムなオフセットで座標を揺らす方法
    • 同じ位置には同じオフセットが適用されるので、時間に対して一貫した歪みを作れる
    • 関連資料としてInigo Quilezの domain warping がリンクされている
  • 歯は parabola() SDFで1本を作ってから mod() で横方向に反復する
    • mod(pixel.x, width) は一定間隔ごとにx座標を再び0から始めさせ、同じ図形を繰り返す
    • pixel.y = abs(pixel.y)-.06 で上下の歯をミラーリングする
    • pow(pixel.x, 2.) ベースのyオフセットで歯を笑みのカーブに合わせる
    • abs(pixel.x+.06) < .194 のような条件で無限反復を制限する
  • 唇と目の下の線は、SDF輪郭線を外側に押し出して描く方式
    • 通常の輪郭線は abs(distance_to_shape) < thickness
    • 外側に離した輪郭線は abs(distance_to_shape - outset) < thickness
  • 目の下の線は位置条件を追加し、目の下の特定領域にだけ表示する

時間で動きを作る方法

  • 最も単純なアニメーションは sin(time) をコードに入れる方法
    • sin() は増え続ける time の値を -1 から 1 の間に包み込み、反復アニメーションを作る
    • sin(time)*.5 + .5 のようにスケールとオフセットで範囲を調整する
  • Rickの首の回転、舌の回転、眉の高さはこの方法で動かす
    • 回転計算には rotateAt() 関数が追加される
  • まばたきは時間に応じて 描画対象を切り替える方法 で実装する
    • mod(time, 2.) < .09 なら閉じた目を描く
    • そうでなければ開いた目と瞳を描く
  • 瞳の不規則な動きには noise() が使われる
    • 目がずっと滑らかに動き続けないよう、round(time) を適用してから noise() に渡す
  • 髪は 時間ドメインワーピング(time domain warping) でさらに柔らかく動く
    • 空間を歪める代わりに、位置に応じて時間値を遅延させる
    • 髪の先端に近いほど時間遅延が変わり、全体が硬く回転するのではなくしなるように見える

ポータル背景と仕上げ

  • 最終版ではRickの背後にポータル効果を追加して完成させている
  • ポータル効果はShaderToyユーザー valena の作った効果をベースにしており、脚注では FabriceNeyret2 が短くした版だと記されている
  • コードは性能より 可読性 を優先している
  • 全体として、2Dシェーダーアニメーションに必要な図形の組み合わせ、SDF、ワーピング、反復、時間ベースの切り替え、ノイズベースの動きを1つのサンプルにまとめている

動画exportワークフロー

  • ページのエディタはまだアニメーションを動画として書き出せない
  • 暫定手段として glslviewerffmpeg を使うmacOSワークフローが提示されている
  • 依存関係のインストール例は以下の通り
brew install glslviewer ffmpeg
  • exportスクリプトは一時ディレクトリを作成し、glslViewer をheadlessモードで実行する
    • 解像度: 1920x1080
    • シーケンス: 0秒から 7秒まで
    • フレームレート: 60
    • 出力ファイル: animation.mp4
  • ローカルのライブコーディング例では次のコマンドを使う
glslViewer shader.frag -w 575 -h 324 --noncurses -x 0 -y 0

スーパーサンプリングと記事の出発点

  • 図形のエッジが滑らかなのは、裏で スーパーサンプリング を適用しているため
  • スーパーサンプリングは画面上の1ピクセル内の9か所で color_for_pixel() を呼び、その平均を表示する方式
  • #version 300 es を使うとエディタの「pro」モードが有効になり、自動スーパーサンプリングが取り除かれる
  • 記事の出発点は、8か月前に公開した動画 I Made a 3D Modeler, in C, in a Week だった
    • その動画には marching cubes アルゴリズムを説明するアニメーションが含まれている
    • 一般的なアニメーションソフトでは正確かつ素早く作るのが難しいと感じ、コードで作り始めた
    • その後、どうやってアニメーションを制作したのかを尋ねられることが増え、この記事にまとめた

1件のコメント

 
GN⁺ 2025-02-08
Hacker Newsのコメント
  • なめらかなアンチエイリアス境界を2回目のスーパーサンプリングパスなしで得たいなら、SDFで標準微分を使えばよい
    基本的には step 関数を aastep に置き換える方式: https://github.com/glslify/glsl-aastep
  • 本当にすごい。シェーダー開発者は別次元の存在みたいだ
    多くの人が慣れているWeb、プロトコル、アプリケーション開発とはまったく違う、緻密で反復的な作業スタイルだ
    float を1つ変えて shift-enter を押すとすぐ結果が見えるのは、かなり気持ちいい
    • JavaScriptの canvas や、その上の p5.js のような抽象化で作業するのもこんな感じなのか気になる
      グラフィックスプログラミング全般の話なのか、それともGPUシェーダー作業をより具体的に指しているのか、よく分からない
  • GLSL入門として非常によく構成された記事だ
    Inigo Quilezの関連YouTubeプレイリストもある: https://www.youtube.com/watch?v=0ifChJ0nJfM&list=PL0EpikNmjs...
  • 参照画像をプレビュー上で点滅させ、コード修正中に元画像と比較したというやり方は、手描きアニメーションで使う手法とまったく同じだ
    シェーダープログラミングは本当に別世界で、記事もとても素晴らしい
  • GLSL入門として構成がとても良かった
    これを VulkanWebGPU/WebGL でやるとどんな感じになるのか気になる
    • ほとんど同じ。VulkanとWebGLはいずれもGLSLを直接使える
      Vulkanは正確にはGLSLからSPIR-Vへ変換する形だ
      ブラウザで動くWebGPUは技術的にはGLSLを直接使えないが、ネイティブのWebGPU実装はGLSLを受け取れるし、変換も可能だ
      あるいは単に WGSL を使えばよく、C風の構文ではなくRustに着想を得た構文である点を除けば、GLSLとほとんど同じだ
  • このアニメーションを作るのに8か月かかったというのは、ものすごい粘り強さを示している
  • 開発過程は小数値をひたすら調整する反復だったのか、それとも何かエディタを使ったのか気になる
    適切な十進数値を持つ240行を試行錯誤で作る方法は、かなり時間がかかったはずだ
    • 単にそのページに埋め込まれているコードエディタを使った
      二分探索は手作業でも速い
    • こういう作業をするときは、普通はスライダーや入力値を1つ用意してユニフォームに接続する
      ユニフォームはシェーダーに渡され、再コンパイルしなくても更新できる