グラフィックスパイプラインについてのメモ

グラフィックスパイプラインは, データフローに着目した抽象的なパイプラインであり, 実際のGPUハードウェア内での部品と必ずしも対応関係が取れているわけではない.

現在のゲームアプリ市場は, OpenGL ES 2.0ベースであるため, OpenGL ES 2.0におけるパイプラインは基本型である.

OpenGL ES 2.0のパイプラインは以下の要素からなる.

  • CPUでのドローコール生成
  • バーテックスシェーダ
  • プリミティブアセンブリ
  • ラスタライゼーション
  • フラグメントシェーダ
  • ROP処理
  • フレームバッファ

このうち, バーテックスシェーダとフラグメントシェーダはプログラマブルである.

CPUでのドローコール生成

ここは, ゲームエンジンがCPUを利用してメモリ上にあるシーングラフ情報をもとに, GPUに与えるデータをセットアップするステージである. シーングラフから描画対象のジオメトリやマテリアルを描画コマンドのバッファに入れ, グラフィックAPIを通じてドローコールを発行する.

OpenGL ESが定義する, GPUへ送られるデータは, 主にポリゴンメッシュを構成する頂点情報と, また, 個々の頂点要素に

  • 座標
  • 法線ベクトル
  • テクスチャ座標
  • (テクスチャが適用されない場合は)頂点カラー情報

などの頂点情報(vertex attribute)が付随する.

テクスチャ座標とは, ある頂点に適用されるテクスチャ画像内の位置がどこかを示す座標である. 0から1に正規化された二次元座標で, テクスチャ画像の左下を原点とする.

Unityでいえば, Meshクラスのuvプロパティにあたる.

バーテックスシェーダ

このステージでは, ジオメトリデータの入力に, ユーザによって作成されるバーテックスシェーダプログラムを実行する. プログラムとして欠かせないのはモデル・ビュー・プロジェクション変換である. 最も単純なバーテックスシェーダは, 座標変換のみを行って, 頂点情報を次のステージに引き渡すだけであるが, 他にも任意の処理を行うことができる.

バーテックスシェーダでは, 頂点の生成・消滅を引き起こすことはできないが, 頂点情報を操作することは可能である. 例えば, 元の頂点情報を書き換えて, 元のポリゴンの形状を変形したり, 頂点カラーを変更して照明効果を適用したりできる. テクスチャ座標もこのステージで変更できるので, テクスチャをスクロールさせたり, スプライトアニメーションを実装したりもできる. スプライトシートやテクスチャアトラスにテクスチャをまとめておけば, マテリアルやテクスチャの変更を減らすことができるため, オーバーヘッドを削減することができる.

プリミティブアセンブリ

バーテックスシェーダの出力は, 描画順に頂点情報が入った配列である. このステージでは, 最初にグラフィックAPIが呼び出された時に頂点の配列と一緒に渡される頂点インデックス(vertex indices)に, バーテックスシェーダから来た頂点配列内の頂点群を対応させ, プリミティブの構築を行う.

プリミティブとは, 点や直線・三角形といった, 頂点からなる基本的な図形である.

プリミティブが全て集まったら, クリッピング(Cliping)が行われる. クリッピングとは, プロジェクション変換で求めたクリップ座標系を用いて, クリップ空間の外側にあるプリミティブを破棄する処理である. もし一部がクリップ空間の外に出ているプリミティブの場合は, クリップ空間の境界に沿うように新しい頂点を生成し, 外側の頂点を破棄する. クリッピング後に透視除算とビューポート変換が行われる.

このステージの最後には, 背面除去(back-face culling)が行われる. これは, 裏側を向いているプリミティブを破棄する処理である.

ジオメトリパイプラインはここまでとなり, ここからは次のステージを挟んで, ピクセルパイプラインへとうつる.

ラスタライゼーション

このステージでは, プリミティブをフラグメント(fragment)へ変換する. フラグメントとは, フレームバッファ1ピクセル分を描画するための

  • スクリーン座標系での位置
  • テクスチャ座標
  • 深度情報

などのデータからなる構造体を指す.

ラスタライゼーションでは, バーテックスシェーダの出力として渡ってくるvarying変数がフラグメント間で補間される. 例えば, 頂点カラーやテクスチャ座標をバーテックスシェーダ内でvarying変数に入れておくと, 色情報や位置情報が頂点間で補間される.

ラスタライゼーションは, プリミティブジオメトリを, ラスタデータに変換する処理である.

一方で, ラスタライゼーションを行わない方式としてレイトレーシング(raytracing)というアルゴリズムがあり, 再帰的に反射する光線を計算してグラフィックスレンダリングを行う. レイトレーシングは, シーングラフへのランダムアクセスが起こり, また, ラスタライゼーションと比べると膨大な演算を発生させるために, 現時点での複雑なリアルタイムレンダリングには向かない. しかし, 写実性においてはラスタライゼーションとくらべ大きく優る.

フラグメントシェーダ

このステージでは, フラグメントに対して, ユーザが用意したフラグメントシェーダの処理を適用する.

フラグメントシェーダの入力は主に, フラグメントの座標のほかに, サンプラという, テクスチャデータを参照するためのuniform変数がある. フラグメントシェーダの出力先は, フレームバッファのピクセルを表す色情報のgl_FlagColor組み込み変数で, RGBの3つの要素と, 透過度を表すアルファチャンネルの4次元ベクトルである. 単に色を計算するよりは, バーテックスシェーダよりも複雑な陰影表現を実現するために使われる.

ROP(render output unit)処理

このステージでは, 各フラグメントごとに各種チェックを順次実行し, フレームバッファへ出力をおこなう.

まず最初に行われるのがシザーテスト(scissor test)で, 全体のビューポート領域内のサブセットである句形部分(シザー領域)外のフラグメントを破棄する.

次はステンシルテスト(stencil test)が行われる. あらかじめ描画前にステンシルバッファ(stencil buffer)という, フラグメントごとに整数値が入ったバッファを設定しておくと, ステンシルテストの段階でビット演算を行い, テスト結果に応じてフラグメントの描画を制御をすることができるため, 内容次第で任意の形状にマスク処理ができる. Unityでは, ShaderLabの文法の, Passブロック内に, Stencilというブロックを記述する.

最後のテストはデプステスト(depth test)で, Zバッファ(z-buffer)と呼ばれる, 各フラグメントごとに深度情報が保存されたバッファが利用される. Zバッファには, ビューポート変換時のzの値が格納されているため, 視錐台の手前からどれだけ奥に位置しているかが保存されている. Unityでは, Passブロック内のZTestブロックで挙動を制御でき, Offsetパラーメータのfatorunitsで深度の値を設定できる. ここで, 深度値は Zの最大の傾き * factor + 深度値の最小単位 * unitsで計算される.

Zファイティング(z-fighting)

Zバッファ法を使った場合に, 板状のポリゴンがデプスバッファの浮動小数点での制度上, 同じとみなされる深度に存在すると, どちらかのポリゴンが手前にあるのかが短い頻度で切り替わり, 画面上でちらつく問題が発生する.

Unityでは, 一方のオブジェクトのシェーダのOffsetを, -1, -1のように微妙にずらしておけば, この問題を回避できる.

早期デプステスト

近年のGPUでは, フラグメントシェーダがgl_FlagDepthに格納されている震度情報を操作しないとうの条件をみたす場合に限り, 早期デプステスト(early depth test)をフラグメントシェーダステージ前に実施し, (コストの高い)フラグメントシェーダプログラムが無駄に実行されないようしている.

早期デプステストを効果的に行うには, 手前にあるオブジェクトの深度情報からテストされると, 最終的に残る最も手前のフラグメントの震度情報が早い段階でデプスバッファにセットされやすくなるため, 無駄なオーバードローを最小限にできる. そこで, オブジェクトの描画順を制御するレンダーキュー(render queue)をビュー空間のシーングラフ内の手前のオブジェクトが前に来るようにソートしておく.

しかし, フラグメントが透明である場合には上記の手法が適用できない. 後ろのオブジェクトがうっすらうつらないといけないため, さきに後ろのオブジェクトを描画する必要がある.

そこで, 透明なオブジェクトについては, レンダーキューを深度が高い順にソートしておき深度が高いものから優先的に描画する必要がある. この方法をZソート法や画家のアルゴリズムと呼ぶ.

参考

渋谷で働くプログラマ。C#をメインに、趣味でGolangとPHPを書きます。スーパーハカーになるべく日々頑張ってます。