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

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

二つのシェーダーとその間

TT Labで続きを見る

目標

頂点シェーダーとフラグメントシェーダーを、Pythonの関数として書き、その間にラスタライザーを置いて、実際に絵を取り出します。このラボが終われば、GLSLのコードを読むとき、各値がどこから来て、どこへ行くかが、見えるようになります。

なぜ重要なのか

シェーダーは、パイプラインのどこでも開いたものではありません。構造はそのままにして、頂点の段階とフラグメントの段階だけを開きました。そうしてはじめて、残り(ラスタライズ、深度判定、ブレンド)を、ハードウェアが引き続き並列で処理できます。

その制約が、シェーダーのルールを作ります。頂点シェーダーは、頂点を作ったり消したりできず、フラグメントシェーダーは、隣のピクセルの結果を見られません。その代わり、2つの段階の間の通り道が明確です。頂点シェーダーがvaryingで出せば、ラスタライザーが重心座標で混ぜて、フラグメントシェーダーに渡します。この構造を、手で一度作ってみれば、GPUプログラミングの制約が、なぜその形なのかが、理解できます。

ステップ

  1. /root/stagesに、ツールボックスを置きます。
  2. /root/stages/vs.pyに、頂点シェーダーを作ります。
  3. /root/stages/varying.pyに、値の補間を作ります。
  4. /root/stages/fs.pyとpipeline.pyに、フラグメントシェーダーを作り、out/uv.pngを作ります。
  5. /root/stages/out/u1.png、u2.png、u3.pngに、ユニフォームだけを変えた絵を書きます。
  6. /root/stages/out/discard.pngに、フラグメントを破棄した絵を書きます。
  7. /root/stages/out/final.pngとout/07-counts.txtに、呼び出し回数を書きます。

参考

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

/root/stages/gfxlib.pyを例のとおりに保存し、/root/stages/check.pyでテストパターンを描いて、/root/stages/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/stages/vs.pyにvertex_shader(attr, uniforms)を作ってください。attrは{"position": (x,y,z), "uv": (u,v)}、uniformsは{"mvp": 4x4 행렬, "scale": 실수}です(プレースホルダーは、4x4の行列と実数です)。位置にscaleを掛けたあと、mvpを適用した4つの成分をgl_Positionとして、uvをそのまま入れた辞書をvaryingsとして、{"gl_Position": ..., "varyings": {...}}を返します。

頂点シェーダーは、頂点1つを受け取って、頂点1つを出します。作ったり消したりできません。そうしてはじめて、ハードウェアが、いくつ処理するかを先に知って、並列に分けられます。

attributeは、頂点ごとに異なる値で、uniformは、ドローコール全体で同じ値です。この区別が、GPUが値をどこに置くかを決めます。

行列の積は、m[r][0]*x + m[r][1]*y + m[r][2]*z + m[r][3]*1を、r=0..3について計算すれば済みます。点なので、wは1です。

varyingsは、フラグメントの段階へ渡す値です。ここでは、uv1つだけですが、実際のシェーダーでは、法線・色・接線などが、あわせて載ります。

ラスタライザーが値を混ぜる

/root/stages/varying.pyにinterpolate(bary, v0, v1, v2)を作ってください。baryは(u, v, w)の3つの実数、v0・v1・v2は、同じキーを持つvaryingsの辞書です。値がタプルなら成分ごとに、実数ならそのまま、u*v0 + v*v1 + w*v2を計算して、同じ形の辞書を返します。

この段階は、シェーダーではなく、ラスタライザーの仕事です。プログラムできない固定の段階ですが、私たちが自分で作ってみる理由は、頂点シェーダーの出力が、フラグメントシェーダーの入力になる通り道が、ここだからです。

値の種類を見分ける必要があります。isinstance(x, (tuple, list))で分ければ済みます。

重心座標の和が1なので、3つの頂点の値がすべて同じなら、結果も同じ値が出ます。この性質で、実装を確認できます。

フラグメントシェーダーと最初の絵

/root/stages/fs.pyにfragment_shader(varyings, uniforms)を作ってください。u, v = varyings["uv"]、k = uniforms["brightness"]とおいて、(255*(0.2+0.8*u)*k, 255*(0.2+0.8*v)*k, 255*(0.2+0.8*(1-u))*k)を返します。そして、/root/stages/pipeline.pyで、頂点3つの(-0.8,-0.8,0)/uv(0,0)、(0.8,-0.8,0)/uv(1,0)、(-0.8,0.8,0)/uv(0,1)を、単位行列のmvp、scale 1、brightness 1で描いて、/root/stages/out/uv.png(256x256、黒い背景)を作ってください。画面座標は、sx=(x*0.5+0.5)*256、sy=(1-(y*0.5+0.5))*256です。

パイプラインの順序は、次のとおりです。頂点ごとに頂点シェーダーを呼び、クリップ座標を画面座標に移し、三角形のバウンディングボックスを走査しながら重心座標を求め、内側ならvaryingを補間して、フラグメントシェーダーを呼びます。

フラグメントシェーダーは、ピクセルごとに呼ばれます。この三角形は、画面の3分の1近くを覆うので、2万回以上呼ばれます。頂点シェーダーは、3回だけです。この差が、性能の話の出発点です。

色の計算に0.2を足してある理由は、uvが0の場所でも、色が0にならないようにするためです。次のステップで、明るさを変えながら比べるときに必要です。

gl_Positionのwが1なので、ここでは除算が恒等です。遠近が入ると、その除算が生きてきます。

ユニフォームだけを変える

形状はそのままにして、brightnessだけを1.0、0.6、0.3に変えて、/root/stages/out/u1.png、u2.png、u3.pngの3枚を作ってください。

頂点データも三角形もそのままです。ユニフォーム1つだけが変わります。

3つの絵で、色が塗られた場所(黒くないピクセルの集合)は、完全に同じでなければなりません。形状が変わっていないからです。異なっていたら、頂点シェーダーやラスタライザーが、ユニフォームの影響を受けています。

一方、色は、目に見えて異なる必要があります。これが、ユニフォームの使い道です。同じメッシュを、別の材質や別の照明で何度も描くとき、頂点データを送り直さずに、ユニフォームだけを変えて描きます。

色の計算に0.2が足されているので、明るさ0.3でも、ピクセルが黒になることはありません。

フラグメントを破棄する

同じ三角形を描きますが、補間されたuvがu*u + v*v > 0.36であるフラグメントを、破棄して、/root/stages/out/discard.pngを作ってください(brightnessは1.0)。

破棄は、フラグメントシェーダーだけができることです。このフラグメントを、まったく使わないと宣言すれば、色バッファーにも深度バッファーにも、何も残りません。

木の葉や金網のように、穴の開いた表面を表現するときに使います。半透明とは違います。半透明は、後ろが透けて見えるように混ぜることで、破棄は、その位置に何も描かないことです。

破棄はタダではありません。discardがあるシェーダーは、色を計算してみないと、生き残るかどうかがわからないので、深度を先に判定する最適化を使えなくします。そのため、穴の開いた表面が、完全に不透明なものより高価です。

破棄された場所は、背景(黒)がそのまま残ります。

どの段階が何回呼ばれたか

破棄をオンにした状態で描き直して、/root/stages/out/final.pngを作り、/root/stages/out/07-counts.txtに、vertices=、triangles=、fragments_in=、fragments_out=の4行を書いてください。fragments_inは、三角形の内側に入ってきたフラグメントの数、fragments_outは、破棄して残った数です。

頂点シェーダーは、頂点の数だけ、フラグメントシェーダーは、画面を覆った面積の分だけ呼ばれます。この三角形は、頂点が3つなのに、フラグメントは2万個を超えます。

そのため、重い計算は、できるだけ頂点シェーダーに移して、補間に任せます。ただし、補間は線形なので、正規化が必要な値は、フラグメント側で、もう一度正規化する必要があります。

fragments_outは、最終的な絵で黒くないピクセルの数と、ちょうど同じでなければなりません。採点ツールが、絵を開いて数えて、突き合わせます。異なっていれば、どちらかが間違っています。

数字をでっち上げず、ループの中で数えてください。