TT Lab
はじめる
学ぶ 学習パス コース

シェーダーとGPUパイプライン

距離が分かれば描ける

TT Labで続きを見る

一言でいうと

ある点から表面までの最短距離を教えてくれる関数があれば、レイを、その距離ずつ安全に前進させて、表面を見つけられます。三角形も、頂点バッファーも必要ありません。

なぜ必要なのか

ラスタライズは、三角形を前提にします。形を変えるには、頂点を作り直す必要があり、2つの形を合わせたり引いたりするのは、形状の演算なので、面倒です。滑らかに溶けてくっつく2つのしずくを、三角形で表現するには、毎フレーム、メッシュを作り直さなければなりません。

距離関数は、形を数式で表現します。球はlength(p) - rの一行で、2つの形の和集合は、min(a, b)の一行です。滑らかにつなげるのも、数行で済みます。データがないので、メモリもほとんど使わず、形を変えることが、そのまま式を変えることです。

どう動くのか

符号付き距離関数(SDF)は、点1つを受け取って、その点から表面までの最短距離を返します。内側なら負、外側なら正、表面の上なら0です。次のコードブロックの韓国語は、球、平面、ボックスという意味です。

구      length(p) - r
평면    p.y - h
상자    q = abs(p) - b
        length(max(q, 0)) + min(max(q.x, q.y, q.z), 0)

核心は、この値が安全な前進距離だという点です。今の位置から、最も近い表面がdだけ離れているなら、どの方向に行っても、dだけは何にも出会いません。そのため、レイをdだけ飛ばしても、表面を通り過ぎることはできません。これを繰り返すのが、スフィアトレーシング(sphere tracing)で、よくレイマーチングと呼ばれます。次のコードブロックの韓国語は、繰り返し、当たった、最大距離、外れたという意味です。

t = 0
반복:
    d = scene(ro + rd * t)
    if d < eps: 맞았다
    t += d
    if t > 최대거리: 빗나갔다

空いた空間では大きく飛ばし、表面の近くでは少しずつ近づきます。そのため、表面をかすめるように通るレイが、最も高価です。距離が小さいままなので、ステップが細かく分割されます。反復回数を色で塗ってみると、シルエットの縁が明るく光る絵が出ますが、性能の問題を目で見る、良い方法です。

形を合わせることも、関数で行います(次のコードブロックの韓国語は、和集合、積集合、差集合、aからbをくり抜く、という意味です)。

합집합    min(a, b)
교집합    max(a, b)
차집합    max(a, -b)          # a 에서 b 를 파낸다

スムーズな和集合は、2つの距離を混ぜます。

h = clamp(0.5 + 0.5 * (b - a) / k, 0, 1)
mix(b, a, h) - k * h * (1 - h)

最後の項が、つながる部分を膨らませて、滑らかな接続を作ります。kが大きいほど、より広く溶けてくっつきます。

法線は、別に保存せず、距離関数の勾配で求めます。各軸にごくわずかに動かして、距離の差を見れば済みます。距離関数の勾配は、定義上、最も急に遠ざかる方向、つまり表面の外向きの方向です。

影も、レイ1本で済みます。当たった地点から、光源のほうへ、もう一度マーチングして、何かに当たれば日陰です。ラスタライズで、シャドウマップをベイクして座標を変換するときに出会う、あらゆる問題が、ここにはありません。その代わり、画面のピクセルごとに、レイを2回撃つことになります。

性能の性質も、ラスタライズとは違います。ラスタライズのコストは、三角形の数と覆ったピクセルの数に付きますが、レイマーチングのコストは、ピクセルごとのステップ数に付きます。そのため、画面の解像度がそのままなら、形をどれだけ複雑にしても、コストは大きく増えませんが、解像度を2倍にすると、4倍になります。形が複雑になるときに遅くなるのは、距離関数自体が重くなるときだけです。

空間の繰り返しという、おもしろい性質もあります。点の座標に剰余の演算をかけて、距離関数に渡すと、同じ形が無限に繰り返されるシーンが、関数一行で作れます。メモリは、1つも余計に使いません。ただし、このとき、距離関数が実際の距離より大きくなることがあるので、繰り返しの周期と形の大きさをあわせて考慮した補正が必要です。

現場での姿

この技法は、シェーダー1つでシーン全体を描くデモシーンと、Shadertoyの文化の中で育ち、今は実務でも使われています。雲や煙のような体積の表現、物体が溶けてくっつく効果、そして、フォントやアイコンを、解像度と無関係にくっきり描く、SDFテクスチャが、それです。

注意する点は、距離関数が実際の距離より大きくてはいけないということです。大きく取ると、レイが表面を飛び越えて、穴が開きます。そのため、形を不均等に引き伸ばしたり、繰り返させたりするときは、距離を保守的に小さくする補正が必要です。

最後に、ラスタライズとどう混ぜるかを、書いておきます。実務で、シーン全体をレイマーチングで描く場合はまれで、普通は、画面を覆う四角形1つをラスタライズしたあと、そのフラグメントシェーダーの中で、マーチングを回します。そうすれば、深度バッファーに結果を書き込んで、三角形で描いた物体と、自然に混ぜられます。雲や煙のような体積の効果が、こうして作られます。形は距離関数で、残りのシーンは三角形で描き、2つを深度でまとめるのです。

次のラボですること

距離関数をいくつかと、合わせる演算を作り、スフィアトレーシングで表面を見つけます。シルエットだけを描いたあと、勾配の法線で陰影を入れ、積集合と差集合とスムーズな和集合を組み合わせて作った形を描きます。最後に、床の平面を置いて、影のレイを撃ち、ボールが床に影を落とす絵を取り出します。