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

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

距離関数で影まで

TT Labで続きを見る

目標

距離関数だけで形を定義し、スフィアトレーシングで表面を見つけ、勾配で法線を求めて、陰影と影まで入れます。このパスの最後の絵が、ここで出ます。

なぜ重要なのか

ラスタライズは、三角形を前提にします。形を変えるには、頂点を作り直す必要があり、2つの形を合わせたり引いたりするのは、形状の演算なので、面倒です。距離関数は、形を数式で表現します。球は一行、和集合はminの一行です。

そして、その値が安全な前進距離だという性質が1つあることで、アルゴリズム全体が成り立ちます。最も近い表面がdだけ離れていれば、どの方向に行っても、dだけは何もないので、レイをその分だけまるごと飛ばしても、表面を通り過ぎることはできません。法線は勾配で求め、影は、レイをもう一度撃てば済みます。ラスタライズで、別のデータ構造や、複数の段階が必要だったことが、ここでは同じ関数1つで解けます。

ステップ

  1. /root/raymarchに、ツールボックスを置きます。
  2. /root/raymarch/sdf.pyに、距離関数と合わせる演算を作ります。
  3. /root/raymarch/march.pyに、スフィアトレーシングを作ります。
  4. /root/raymarch/out/sphere.pngに、シルエットを書きます。
  5. /root/raymarch/out/shaded.pngに、勾配の法線とランバートの絵を書きます。
  6. /root/raymarch/out/csg.pngに、削って貼り合わせた形を書きます。
  7. /root/raymarch/out/shadow.pngとout/07-shadow.txtに、影のレイの結果を書きます。

参考

描画のツールボックスを置く

/root/raymarch/gfxlib.pyを例のとおりに保存し、/root/raymarch/check.pyでテストパターンを描いて、/root/raymarch/out/00-check.pngを作ってください。パターンは、64x64の黒い背景に、(0,0)から(63,63)まで、白(255,255,255)の対角線を引き、そのあとに、(0,32)から(63,32)まで、赤(255,0,0)の横線を重ねて引いたものです。

このラボからは、PNGエンコーダーを作り直しません。最初のラボで手で作ったものと同じコードを、ツールとして配ります。ここで学ぶのは、ファイル形式ではないからです。

ラボのPodにはボリュームがないので、前のラボで作ったファイルが残っていません。そのため、ラボごとに、ツールボックスを置き直すことから始めます。

Canvas(w, h, bg)を作り、line(x0, y0, x1, y1, rgb)で2本の線を引いたあと、write_png(path)で保存してください。交差点(32,32)が赤になるには、横線をあとで引く必要があります。

このラボでは、レイマーチングの結果を、絵として書き出すのに使います。

距離関数と合わせる演算

/root/raymarch/sdf.pyに、sphere(p, r)、box(p, b)、plane_y(p, h)、op_union(a,b)、op_intersect(a,b)、op_subtract(a,b)、smooth_union(a,b,k)を作ってください。ボックスは、q = abs(p) - bとおいて、length(max(q,0)) + min(max(qx,qy,qz), 0)で、差集合はmax(a, -b)、スムーズな和集合は、h = clamp(0.5 + 0.5*(b-a)/k, 0, 1)について、b*(1-h) + a*h - k*h*(1-h)です。

符号付き距離関数は、内側なら負、外側なら正、表面の上なら0です。球は、length(p) - rの一行で、その性質を満たします。

ボックスの式が2つの項になっている理由は、外側と内側を分けて扱うからです。外側では、各軸の超過分をベクトルとみなして長さを測り、内側では、最も近い面までの距離(負)を使います。

差集合の-bが意味するのは、bの内外を反転させることです。反転させたbとaの積集合が、そのまま「aからbをくり抜いたもの」です。

smooth_unionの最後の項-k*h*(1-h)が、つながる部分を膨らませて、滑らかにします。この項がないと、ただの線形補間なので、継ぎ目が角ばって残ります。

スフィアトレーシング

/root/raymarch/march.pyにmarch(ro, rd, scene, max_steps=64, max_dist=20.0, eps=0.001)を作ってください。t=0から始めて、d = scene(ro + rd*t)を求め、d < epsなら(True, t, 걸음수)を返し(プレースホルダーは、ステップ数です)、t > max_distになれば、(False, t, max_steps)を返します。そうでなければ、t += dで前進します。

距離関数が教えてくれる値は、どの方向に行っても安全な距離です。そのため、その分だけまるごと飛ばしても、表面を通り過ぎることはできません。これが、このアルゴリズムが成り立つ理由のすべてです。

空いた空間では大きく飛ばし、表面の近くでは少しずつ近づきます。そのため、表面をかすめるように通るレイが、最も高価です。ステップが細かく分割されます。

(0,0,-5)から(0,0,1)の方向に、原点の半径1の球に向けて撃つと、tが4の近くで止まります。epsの分だけ誤差があるので、ちょうど4ではありません。

ステップ数もあわせて返してください。次のステップで、コストを目で見るのに使えます。

まずシルエットから

/root/raymarch/render.pyで、160x160の絵/root/raymarch/out/sphere.pngを作ってください。カメラは(0,0,-4)にあり、ピクセル(x,y)のレイの方向は、normalize((px, py, 1.5))、px = (x+0.5)/160*2 - 1、py = -((y+0.5)/160*2 - 1)です。シーンは、原点の半径1の球だけで、当たれば白(255,255,255)、そうでなければ黒です。

pyにマイナスが付くのは、画像のyが下に向かって増えるからです。このパスで、ずっと出てきた、その問題です。

1.5は、焦点距離にあたります。大きければ視野が狭くなり(望遠)、小さければ広くなります(広角)。前のコースの視野角と同じものを、別の方式で書いたものです。

160x160なら、レイが25,600本です。Pythonでも、1秒以内に終わります。距離関数が、空いた空間を大きく飛ばすからです。

円の直径が、画面の半分くらいになる必要があります。はるかに小さいか大きければ、カメラの距離や焦点距離を確認してください。

勾配で法線を求める

/root/raymarch/shade.pyで、同じ球に陰影を入れて、/root/raymarch/out/shaded.pngを作ってください。法線は、e=0.001で、各軸の距離の差を求めて、正規化したもので、光はnormalize((-0.6, 0.7, -0.5))、ambient 0.15、基本色(230,180,120)で、明るさは、ambient + (1-ambient)*max(0, dot(n, l))です。

距離関数の勾配が、そのまま法線です。距離が最も急に大きくなる方向が、表面の外向きの方向だからです。法線を別に保存する必要がないことが、この方式の利点の1つです。

各軸ごとに、scene(p + e) - scene(p - e)を求めれば済みます。中心差分なので、1つの軸に2回、合計6回、距離関数を呼びます。

eを小さくしすぎると、浮動小数点の誤差が大きくなり、大きくしすぎると、法線がつぶれます。0.001が、この大きさのシーンに合っています。

当たった地点は、ro + rd*tです。光が左上から来るので、球の左上が明るい必要があります。

形を削って貼り合わせる

/root/raymarch/csg.pyで、次のシーンを、/root/raymarch/out/csg.pngに描いてください。base = op_intersect(box(p,(0.85,0.85,0.85)), sphere(p,1.1))で、角を丸めた立方体を作り、base = op_subtract(base, sphere(p-(0,0,-1.0), 0.6))で、正面をくり抜いたあと、smooth_union(base, sphere(p-(1.0,0.9,0.0), 0.45), 0.35)で、小さな球を滑らかにつなぎ合わせます。陰影は、ステップ5と同じです。

3つの演算が、それぞれ何をするかを見てください。積集合は、2つの形が重なる部分だけを残して、角を丸め、差集合は、穴を開け、スムーズな和集合は、継ぎ目を溶かします。

形を変えることが、そのまま式を変えることです。頂点バッファーも、インデックスバッファーも、作り直す必要がありません。三角形で同じことをするには、形状演算のライブラリが必要です。

座標を移した球は、sphere(p - 중심, 반지름)と書きます(プレースホルダーは、中心と半径です)。点を移して測ることが、形を移すことと同じです。

ステップ5で作ったrender(scene, path)を、シーン関数だけを変えて、そのまま使ってください。

影のレイ

シーンをop_union(sphere(p,1.0), plane_y(p,-1.0))として、当たった地点から、法線方向に0.02浮かせた位置から、光の方向へ、max_dist=10.0で、もう一度マーチングして、何かに当たれば、ambientだけを適用してください。結果を/root/raymarch/out/shadow.pngに書き、/root/raymarch/out/07-shadow.txtに、hits=、shadowed=、fraction=の3行を書いてください。fractionは、shadowed / hitsです。

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

法線方向に少し浮かせることが重要です。表面のすぐ上から出発すると、最初のステップで自分自身に当たって、すべてのピクセルが日陰になります。この問題を、シャドウアクネ(shadow acne)と呼び、ラスタライズのシャドウマップでも、同じ名前で現れます。

床の平面を入れたので、画面の下のほうが床です。ボールが床に影を落とす様子が出る必要があります。

日陰の割合は、6パーセント前後です。はるかに大きければ、浮かせる量が足りず、0なら、影のレイが常に外れています。