2 ポイント 投稿者 GN⁺ 2023-10-27 | 1件のコメント | WhatsAppで共有
  • ホームページのシンプルな CSS border-radius アニメーションを出発点に、数行の fragment shader で滑らかな blob アニメーションを自作している
  • シェーダーは GPU 上でピクセル座標を色に変換する小さなプログラムであり、並列実行 によって高速だが、状態共有や高水準の抽象化には制約がある
  • GLSL の基本例では vUv 座標を gl_FragColor の RGBA 値にマッピングし、varyinguniform、ベクトル型、swizzling といった文法を学ぶ流れになっている
  • 円や blob は distance()step()smoothstep()Signed Distance Function(SDF) によって作り、複数の SDF を min()smooth minimum で結合する
  • u_time で球の動きを作り、u_mouse を中心配列に追加すると、ユーザーがマウスで一部を操作できる インタラクティブなシェーダー に拡張できる

シェーダーの役割と制約

  • シェーダーは GPU 上で実行される小さなプログラムで、少なくともピクセル座標を入力として受け取り、 を出力する
  • ビデオゲームでは照明、特殊効果、トゥーン調レンダリングのような視覚効果に使われ、現代のゲームグラフィックスを支える重要な基盤になっている
  • 速度の核心は、多数のピクセルで同時に実行される 並列化 にある
  • この入門では、ブラウザで扱いやすい OpenGL Shading Language、すなわち GLSL に焦点を当てる
  • 性能の代償として、シェーダーは小さく低水準で記述する必要がある
    • 高水準の抽象化やライブラリの import に頼りにくい
    • 並列実行の性質上、ピクセル間でのデータ保存や共有ができない ステートレス・メモリレス な方式である

座標を色に変える最初の GLSL 例

  • シェーダーは正規化された座標を RGBA 色に変換する
    • 座標は通常 0 から 1 の範囲に正規化される
    • (0, 0) は左下、(1, 1) は右上を表す
    • この座標は慣例的に st または uv と呼ばれる
  • 最も単純な例は、x 座標が大きいほど赤、y 座標が大きいほど緑が増える グラデーション である
varying vec2 vUv;

void main() {
  vec2 st = vUv;
  gl_FragColor = vec4(st.x, st.y, 0.0, 1.0);
}
  • 青のグラデーションは gl_FragColor = vec4(0.0, 0.0, st.x, 1.0); のように青チャンネルへ x 座標を入れて作る
  • 文法で重要な要素は次のとおり
    • varying: ピクセルごとに値が変わる入力
    • uniform: すべてのピクセルで共通の入力
    • vec2, vec3, vec4, mat2, mat3: C のように型を明示するベクトル・行列型
    • swizzling: vec4(1, 2, 3, 4).xy のようにベクトルの一部を取り出す記法
    • gl_FragColor: main() の最後で各ピクセルの色を決める出力

distance(), step(), smoothstep() で円を作る

  • 円のようなシャープな図形を描くときも、drawCircle() のような関数ではなく 数学的な距離 を使う
  • 現在のピクセルと円の中心の距離は distance(vec2 p1, vec2 p2) で計算できる
  • 距離をそのまま色にマッピングすると円形のグラデーションになり、塗りつぶされた円は step(float threshold, float value) で作る
    • 距離がしきい値を超えると 1
    • そうでなければ 0
  • step() は切り替えが急なため、円の縁に aliasing が出ることがある
  • より滑らかなエッジは smoothstep(float t_start, float t_end, float x) で作れる

Signed Distance Function で形状を表現する

  • Signed Distance Function(SDF) は、空間内のある点が図形からどれだけ離れているかを符号付き距離で表す
    • 図形の内側は負
    • 外側は正
    • 境界は 0
  • 円の SDF は、中心からの距離から半径を引く形で作れる
float circleSDF(vec2 p, float r) {
    return length(p) - r;
}
  • ピクセル位置 uv を基準に点を移動すれば、UV 空間の任意の位置にある円までの距離を計算できる
  • d < 0.0 であれば、そのピクセルは円の内側なので別の色で塗れる
  • さらに多様な 2D SDF 図形は Inigo Quilez の comprehensive list で見られる

複数の SDF を合成して blob を作る

  • SDF は ブール演算 によって新しい図形を作りやすい
  • 2 つの SDF の和集合は、2 つの距離の min() を取ればよい
    • 2 つの図形のどちらか一方でも内側なら、最小距離は負になる
    • 両方とも外側なら、最小距離は正になる
  • 1. - smoothstep() を使う理由は、step()smoothstep() がしきい値より大きい距離、つまり図形の外側で 1 を出力するためである
  • 単純な min() は、2 つの円が接する部分に鋭い不連続を作る
  • blob のように滑らかに混ぜるには smooth minimum を使う
    • 追加引数 k が滑らかさの強さを制御する
    • 例では polynomial smooth min 関数を使っている
float smin(float a, float b, float k)
{
    float h = max( k-abs(a-b), 0.0 )/k;
    return min( a, b ) - h*h*k*(1.0/4.0);
}

時間 uniform で metaball アニメーションを作る

  • シェーダーには、スライダーのような任意の値を uniform として渡せる
  • アニメーションは、JavaScript で作った u_time をシェーダー入力に渡し、それを円の中心座標の計算に使う形で実現する
  • シェーダーは基本的に毎秒 60 回、新しい u_time 値で更新され、滑らかな動きを生み出す
  • 円の中心は sincos のような周期関数で揺らせる
  • 複数の円を metaball として結合するときは、中心座標を配列に保存し、ループで SDF を累積する
vec2 centers[4] = vec2[4](c1,c2,c3,c4);

float d = 99.;

for (int i = 0; i < 4; i++) {
  vec2 c = centers[i];
  float sdf = circleSDF(uv, c, .1*u_slider);
  d = smin(d, sdf, K);
}
  • 結果の blob は機能的には動作するが単色なので、その後に色とインタラクションを追加する

マウスで操作する最終インタラクション

  • 最終段階では、マウス座標を u_mouse uniform として受け取り、blob 内の球の 1 つをユーザーが直接操作できるようにする
uniform vec2 u_mouse;
  • マウス座標を球の中心配列に追加すると、1 行で インタラクション が入る
vec2 centers[5] = vec2[5](c1,c2,c3,c4,u_mouse);
  • 最終シェーダーでは、マウスの y 軸をキャンバス座標に合わせて反転し、動く 4 つの中心とマウス中心をまとめて結合する
  • 色は mix(colorA, colorB, percent) を何度も使って合成する
    • percent を boolean のように使うと、if/else に近い動作になる
    • metaball の値、中心点までの距離、shine、membrane の計算が最終的な色に反映される
  • 同じ原理を理解すれば、Blender’s shader nodesUnity’s Shader Graph のようなノードベースのシェーダーエディタの内部動作もより理解しやすくなる

さらに見るべき資料

1件のコメント

 
GN⁺ 2023-10-27
Hacker News のコメント
  • インターネットに文章を書き、自分を表に出す勇気をようやく出した。以前からシェーダーを学びたいと思っていて、学習過程を文書化して他の人と共有できたらいいと考えていた

    • 個人ブログに記事を書き、インタラクティブな記事にしている点が本当にいい。ひどい Notion/Medium/Dev のフィードで Google のアルゴリズムや AI のコピペを養うやり方とは違う。
      誠実なブログは全能のアルゴリズムの影の中で、見えないまま死にかけていて、インターネットは Star Wars エピソード IV のど真ん中まで来てしまった感じがする。フィードバックは一つだけ: ありがとう、もっとたくさん書いてほしい
    • インターネットへようこそ、superMayo!
      シェーダーで達人たちが何をできるのか見たいなら、Inigo Quilez と彼のシェーダーアートを紹介したい: https://www.youtube.com/watch?v=BFld4EBO2RE
      追記: 投稿者本人だと気づいていなかった。記事はとてもよくできていて、シェーダーコーディングというアートをもっととっつきやすく、インタラクティブに扱うチュートリアルを探していた
    • 十分サポートされている OES_standard_derivatives 拡張か WebGL 2 を使う気があるなら、アンチエイリアシングを改善できる。smoothstep(0.0f, 0.01f, dist); のように定数を適当に選ぶ代わりに、smoothstep(fwidth(dist), -fwidth(dist), dist); を使えばいい
    • シェーダーが完全にグラフィカルな対象であることを考えると、この記事はシェーダー入門としてほぼ最高のやり方に感じる。インタラクティブなコードが練習用リンクに飛ばされるのではなく、記事の中に直接埋め込まれている方式がとても合っている
  • 記事はかなり良いが、シェーダーの核心的な問題を軽く流しすぎている。
    シェーダーは、ほとんどのプログラムやアプリケーションにとって望まれない厄介者だ。3D は三角形を好み、GPU もその抽象化にうまく合っており、シェーダーはその三角形上で補間するのに役立つ。
    しかし 3D ではないほぼすべての領域では、三角形はあまり良くない。2D レンダリングはパスを求め、フォントレンダリングはパスやピックスマップを求め、GUI にはパスとピックスマップの方がずっと合い、コンポジターはピックスマップを求め、ビデオデコーダーはピックスマップと並列レンダリングを求める。
    非 3D 側が欲しいのは長方形のピックスマップと、そのピックスマップへ直接計算アクセスする方法だが、GPU はそれをあまり好まず、シェーダーもそこにはうまくはまらない

    • 2D 曲線レンダラーを書いた立場からすると、並列計算と高い帯域幅が必要で、GPU はそれを提供してくれる。特に scatter-write をサポートする新しいインターフェースではなおさらだが、ブラウザにどれほど普及しているかはよく分からない。
      高レベルでは望む形ではないというのはその通りだが、より高レベルの抽象化を実装する土台としては十分に良い。ハードウェアで直接サポートすることもできるだろうが、利点が明確とは言えない。CPU が for ループをアーキテクチャレベルでサポートしていないと不満を言う人はいない。
      読み返してみると、GPU について何が問題だと言っているのかよく分からない。頂点処理パイプラインは完全に無視して、全画面の四角形を一つ描くか計算シェーダーを使えばよく、GPU はそれを非常にうまく処理する。リンク先の記事もこの種のことを話している
    • 逆の立場から見ると、ほとんどのプログラムは望む結果を得るのに GPU が必須ではない。一方で 3D は、ごく小さな規模を超えるとそうした並列性がほぼ必須になる。
      少なくとも 2000年代にはそういう考え方だった。ソフトウェアがより複雑になるにつれて GPU が望ましい最適化になり得るが、GPU パイプラインは常に厳格で閉じており、単一コアからマルチコアプログラミングへ移るときのパラダイムは、しばしば別のアルゴリズムを要求する。
      汎用 GPU プログラミングは三角形に縛られないようにしてくれるが、その並列性を享受するには、依然としてまったく別のアプローチが必要だ
    • ピクセル/フラグメントシェーダーのどの部分がピックスマップにうまく対応しないというのか分からない
  • すばらしい! 最近 SDF の沼を少し掘っていた。iq のサイトへのリンクを入れてくれてうれしかったし、あちらの資料は本当に良い。
    彼の “happy bouncing” シェーダーをリンクせずにはいられない。個人的には驚異的だと思う: https://www.shadertoy.com/view/3lsSzf
    制作過程を収めた 6 時間の YouTube 動画もある。約 500 行の中身の濃いコードだ

    • Inigo Quilez は魔法使いで、シェーダーコミュニティは彼に本当に大きな恩がある
  • このテーマに興味を持とうと何度も試したが、入りやすい入口を見つけられずにいたところ、この入門記事で見つけた。本当に楽しく遊び心のあるやり方で気に入ったし、次の記事も早く読みたい

  • 些細な揚げ足取りだが、cel shading に言及しながら cell と書いている。この用語は手描きアニメーションで使われていた cel と、そのシェーディングの量子化されたトーンに由来する

  • 本当に良い。アーティストからプログラマーになった身として、時々グラフィックスプログラミングを掘ってみたい衝動に駆られる。
    ごく基本的なシェーダーはいくつか書いたことがあるが、数学が入り始めると、実際にはかなり早い段階で、壁にぶつかる。コンピューターサイエンスではなく美術を専攻したので、数学力は実質的にないに等しい。
    ともあれ、よく書けていて記事が気に入った

  • すばらしい入門記事で、続いてほしい。こういう記事は見事な導入で始まっておきながら、あまりにもよく途中で止まってしまう

  • シェーダーを扱ったことがないので、かなり初歩的な質問かもしれません。ゲームの1フレームでは、本質的にシェーダーがすべてを描画しているのでしょうか?
    それとも三角形、四角形、円のような基本図形があって、シェーダーがその上に影やエッジ補正のようなものを重ねるのでしょうか?
    例を見ると、シーン内のどんなオブジェクトでも描けるシェーダーを作れて、その後ほかのシェーダーを組み合わせて影や照明のようなものを得るように見えます。私のごく限られた描画経験では、図形を描いていたのであって、シェーダーで描いていたわけではありません。シェーダーがオブジェクト自体を描くわけではないと思ってきました

    • 以下は古典的なフォワードレンダリングの流れを単純化して説明したもので、いわゆる遅延レンダリングは少し異なります
      GPUは、3つの頂点と各頂点に紐づく法線などのデータで定義された三角形のような抽象的なベクター図形を、出力バッファ上でその図形が覆う各ピクセルにつき1つ、マルチサンプリングならそれ以上の、フラグメントのストリームに変換します。この部分はすべてハードウェアが処理します
      フラグメントはピクセル座標とユーザー提供データで構成されます。そのデータは定数である uniform か、前述の頂点データが三角形の面全体にわたって補間された varying です。この補間もハードウェアが処理し、プログラムすることはできません
      フラグメントシェーダーはフラグメントを入力として受け取り、そのデータに応じて色を計算し、さらにいくつかの段階を経て、画面またはオフスクリーンバッファの該当ピクセルの色として出力します。単色の場合もあれば、複雑なライティング計算の場合もあります
      GPUレンダリングでは、これらすべてが大規模に並列実行され、多数のフラグメントが同時に処理されます。シェーダーは純粋で状態を持たない関数なので、入力にしかアクセスできず、できる効果も色や深度値などいくつかを返すことだけです
      要約すると、GPUハードウェアは各三角形を描画するためにどのピクセルを塗るべきかを計算し、フラグメントシェーダーはその各ピクセルの色の値を決定します
    • シェーダーがすべての描画をしているとはいえ、複数の段階に分かれています。全体のグラフィックスパイプライン[1]を説明するつもりはありませんが、人々がライティング効果のための「シェーダー」と大まかに呼ぶもののかなりの部分、90%以上はレンダラーのフラグメント/ピクセルシェーダー段階です
      フラグメントシェーダーがシーンの「上に」描く前に、頂点、テッセレーションなどの別の段階が基本図形を描きます
      フラグメントシェーダーについても、ここで説明したよりはるかに多くの内容があります。たとえば遅延レンダリング[2]がそうで、それ自体も掘り下げると大きなテーマです
      1: https://vulkan-tutorial.com/Drawing_a_triangle/Graphics_pipe...
      2: https://learnopengl.com/Advanced-Lighting/Deferred-Shading
    • その通りで、すべてのピクセルの色は最終的にシェーダープログラムが決めますが、予想どおりそれよりも複雑です
      固定機能のハードウェア段階とプログラム可能な段階が混在したグラフィックスパイプラインがあります。高いレベルでは、1) GPUがCPUから3D三角形の集合を受け取り、2) 頂点シェーダーが3D三角形の頂点をピクセル座標を持つ2D三角形の頂点へ変換して平坦化し、3) GPUが2D三角形をラスタライズして、正確にどのピクセルが三角形に覆われるかを決定し、4) 覆われた各ピクセルごとにピクセルシェーダーが実行されてピクセル色を決定し、5) 結果のピクセル色がフレームバッファに保存されます。このとき既存の色とブレンドされることもあります
      このパイプラインは、フレーム全体が描かれるまで、異なる三角形メッシュとシェーダーで何度も繰り返されます
    • おおむね正しく理解しています。私の知る限り、GPU上で動くあらゆる種類のコードはシェーダーと呼ばれます。たとえば特殊なデータ演算もコンピュートシェーダーと呼ばれますが、性質は異なります
      色、影、シェーディング、画像効果、一般的な画像処理のような作業は、すべて複数のデータ配列を組み合わせる並列計算で行われます。頂点とその属性、元のテクスチャ、事前計算された関数、対象テクスチャ、バッファなどがこれに含まれます
      たとえば光と影を得るには、シェーダーがスポットライトの位置や方向のような、おそらくグローバル変数にアクセスできる必要があります。合成されたライティングは、複数のシェーダーパスを組み合わせて得られることが多いです。グローバルライティング用の基本パス1つと、各ライトごとのパス1つのように構成し、各パスが文字どおり光を足していきます
      ここで光源が遮られたピクセル、つまり影には光を足さないようにするため、最も一般的な手法はZ-bufferというものを使うことです。実質的には浮動小数点テクスチャです。シーン内の各ライトがどこまで届くかを知りたいので、すべてのライティングを適用する前に、シーン内のすべての堅いジオメトリを結合する単一のシェーダーパスを設定し、ライトの位置と方向をカメラ変換のように使い、オブジェクトまでの距離をZ-bufferに書き込むだけの特殊なシェーダーを使います
      その後、空間上のある点に光が届くかを知りたいときはいつでも、少し幾何計算をした後でこのZ-bufferをサンプリングし、その方向に保存された値とその点までの距離を比較します。バグが多くなりがちで、精度誤差もよくあります。きちんとしたエンジンはすでにこの作業をしてくれますが、その過程を変更できるようにもしてくれます
      残りはすべてこのテーマのバリエーションです。遅延レンダリングは、色ではなくデータを中間テクスチャにレンダリングし、後で処理して色を得る方式です。ブラー効果はレンダーテクスチャの2D畳み込み、たとえばガウシアンカーネルで処理されます。テッセレーションシェーダーは、頂点シェーダーで新しいジオメトリを生成することに関係します。テキスト描画でさえ、フォントアトラスと小さな長方形を通じて行われます
    • フラグメント、つまりピクセルシェーダーでどんなオブジェクトでも描けます。技術に関係なく図形を描く特定の数学的手法、たとえばSDFや三角法などを使えるからです
      そのため、才能あるアーティストたちはフラグメントシェーダーの中で限界に挑み、パフォーマンス上のトレードオフと戦っています
      フラグメントシェーダーは、より一般的にはフルスクリーンのフィルター効果、たとえば色補正などに使われます
      シェーダーは基本オブジェクトのテクスチャやマテリアルを作るのにも使われます。マテリアルアーティストは、シェーダーの数学でテクスチャを生成することが多いです
      多くの視覚効果は、シェーダーを創造的に使って作られます
      シェーダーはGPU上で、並列的な波のような方式で実行されます。非常に多くのスレッドが1つのウェーブ内で同じデータを対象に実行されます
      場合によっては、シェーダーはCPUの分岐コードよりはるかに高速です。シェーダーは一部のレンダリングデータにもより簡単にアクセスできます

そのため、創造的な特殊効果を作るのに適した領域です。ゲームで表面ディテールの多いオブジェクトは、そのディテールをシェーダーに移すための一般的な対象です。海面やテッセレーションされたメッシュなどがそれにあたり、GPU は強力で柔軟なため、ほかにも多くの用途があります。

  • 画像がちらつくノイズのように見える人がいるなら、ブラウザで画像をコピーして別の場所に貼り付けることで解決しました。そうすると画像を正しく見られます
    Imgur のリンクです。最初の画像はブラウザで見えている画面のスクリーンショットで、残りは Imgur に貼り付けた後の実際の画像です
    https://imgur.com/a/F4203rz

    • 変ですね。どのブラウザを使っていますか?
  • シェーダーについての私の理解はこうです。
    CPU で線を描くときは、点 A と点 B の間の各ピクセルを巡回しながら、1 ピクセルずつ順番に描く関数です。線上にあるピクセル数と同じだけ正確にステップがあり、1 回実行されます
    GPU で線を描くときは、そのピクセルが線上にあるかを確認し、そうであれば描く関数です。線からかなり離れたピクセルも含め、画面上のすべてのピクセルで同時に実行されます
    これで合っていますか?

    • 正確には違います。第一に、GPU がすべてのピクセルに対してピクセルシェーダーを同時に実行できるわけではありません。一般的な画面は約 200 万ピクセルありますが、GPU の同時実行スレッドは多くても数千個なので、実際には数千ピクセル単位の塊で描画します
      第二に、GPU が画面全体に対してピクセルシェーダーを実行する必要もありません。三角形を使って、目的の任意の図形に対してだけシェーダーを実行できます。なので線を効率よく描く方法は、目的の線のジオメトリに合う 2 つの三角形を GPU に送り、その三角形が重なるピクセルに対してだけピクセルシェーダーを実行することです。はるかに効率的です