■ 2012年09月15日 [OpenGL][GLFW] (5) パイプライン
パイプライン
パイプラインは, えっとなんでしたっけ, そうそうあれあれ, 油田とかから石油を運ぶながーい管のことをいいます. でもコンピュータでは, これは一つの処理をいくつかの段階 (ステージ) に分割して, 処理を順送りしていくことをいいます. 管の中で石油が順送りされていくというメタファですね. 同じ時間がかかる処理でも, 分割した各段階を同時に動作させることで, 全体的な処理量 (throughput) を増加させることができます.
CG の場合は, パイプラインは二通りの意味で用いられます. 一つは前述の意味の, ハードウェア構成上のパイプラインです. そしてもう一つは, 映像生成の手続きにおける, ソフトウェア構成上のパイプラインです*1.
ソフトウェアのパイプライン
ラスタライズによる映像生成では, 物体の形状を頂点とそれらを結んだ線分や三角形で表します. このような形状データから映像を生成するには, まず頂点データを座標変換してスクリーン上の位置を求め, それをもとに画面上で線分や三角形を描画します. 描画は出力となる画像データを構成する画素に色のデータを設定することにより行います.
この処理の手順は, 次の四つの段階に大まかに分けることができます. 第一の段階は座標値などの頂点データ (ジオメトリデータ) に対して座標変換などを行うジオメトリ処理, 第二の段階は変換された座標データを使って形状データの画像化を行うラスタ化処理 (ラスタライズ), 第三の段階は画像化されたデータの個々の画素データ (フラグメントデータ) に対して色などの値を設定するフラグメント処理, 最後の第四の段階は描画する図形ごとに生成される画素データに対して, 隠面消去処理などを行って一つの画像データを合成するラスタ処理です.
ラスタ化処理の前の段階までは, 形状データは頂点とそれらの結びつきのデータで構成された, 幾何学的な構造を持っています. ジオメトリ処理は座標変換や頂点の陰影付け, それに必要に応じて図形の細分化などの幾何学的な処理を行います. 形状データは, この後に行われるラスタ化処理により, 画素の配列のデータに置き換えられます. ラスタ化処理では, 描画する線分や三角形を構成する画面上の画素の選択を行います. これを走査変換 (Scan Conversion) といいます. この処理によって選んだ画素に対して, フラグメント処理を実行します. これは画素単位の演算などの画像処理的な処理になります.
ハードウェアのパイプライン
CG のハードウェアにおけるパイプラインは, このソフトウェアのパイプラインをハードウェア上に実装しようとするものです. しかし, 現実のハードウェアのパイプラインは, ソフトウェアのパイプラインをそのまま実現しているとは限りません. 実際の実装は, たとえば GPU のチップメーカーごとに異なるでしょうし, 技術的な発展に伴ってダイナミックに変化していくものだと思います. そこで, ここではハードウェアの「概念的な」パイプラインを, 次のような単純な構造で考えることにします.
アプリケーション, すなわち CPU 側の処理では, あらかじめジオメトリ処理とフラグメント処理の内容をシェーダプログラムとして記述し, それぞれバーテックスシェーダとフラグメントシェーダに転送します. シーンをどのように描くのかはアプリケーションでも制御しますが, これをシェーダで記述して, できる限り GPU に移譲するのが今風のグラフィックスプログラミングではないでしょうか.
同様に, シーンの形状データもあらかじめ GPU に送ってしまいます. 送るデータは頂点の位置などの頂点情報ですが, 一つの頂点が保持する情報には他に法線ベクトルや頂点色, テクスチャ座標などさまざまなものがあります. また, 場合によっては位置が必要ないこともあります. このような頂点に割り当てられるこのような情報のことを, 頂点属性 (attribute) といいます. これを GPU 側に確保したバッファオブジェクトと呼ばれるメモリに転送します. 頂点属性を保持するバッファオブジェクトを, 特に頂点バッファオブジェクト (Vertex Buffer Object, VBO) といいます.
この後, 描画に使用する基本図形 (primitive, 線分, 三角形など) と, 描画に用いる頂点属性のバッファオブジェクト上の範囲を指定して, 描画命令を実行します. したがって, 描画するすべての物体の頂点属性を一つのバッファオブジェクトにあらかじめ転送しておいて, 描画命令を実行する際にバッファオブジェクト上の範囲を指定して描き分ける, という使い方もできます.
「手抜きOpenGL」はもう15年も前に書いたもので, この頃の OpenGL 1.1 のプログラミングモデルは, まさに「CPU が画面に図形を描く」というものでした. しかし, 特に OpenGL 3.0 以降では, CPU の仕事は「データをそろえて GPU に起動をかける」というイメージでとらえた方がいいかもしれません. いい加減, この「転換」を「手抜きOpenGL」に反映しないとまずいとずっと思っているんですけど…
シェーダプログラム
シェーダプログラムは GLSL (OpenGL Shading Language) というプログラミング言語で記述します. これはグラフィックスハードウェアのパイプラインに沿って, いくつかのプログラムを組み合わせて構成します. 以下に前回追加したバーテックスシェーダとフラグメントシェーダのソースププログラムを示します. OpenGL 3.2 の場合は, これにジオメトリシェーダを追加することができます. これについては後に説明します.
ソースプログラムの1行目にある #version の行は, GLSL のバージョンを示します. 150 は OpenGL 3.2 の GLSL のバージョン 1.5 を表します. core は Core Profile であることを示します. なお, このバージョンまでは OpenGL と GLSL のバージョン番号が対応していないのですが, OpenGL 3.3 以降では一致するようになりました. OpenGL 3.3 の GLSL のバージョン番号は 3.3 で, #version 行のバージョンの標記は 330 になります.
シェーダプログラムは C 言語同様 main() 関数から実行を開始します. ただし, シェーダプログラムの main() 関数は引数を受け取らず, 値を戻すこともありません. したがって, 関数の定義は void main(void) になります.
バーテックスシェーダのソースプログラム
#version 150 core in vec4 pv; void main(void) { gl_Position = pv; }
pv はバーテックスシェーダに入力された頂点属性 (attribute) を受け取る変数です. このような変数を attribute 変数といいます. in はこのシェーダプログラムの入力となる読み出し専用の変数であることを示します. vec4 は 32bit 浮動小数点 (float 型) 4 要素からなるベクトル型です. 描画命令が実行されると, バッファオブジェクトの頂点属性の一つの頂点ごとに取り出し, それを attribute 変数に渡してバーテックスシェーダを起動します. バッファオブジェクトを attribute 変数に対応づけるには, glBindAttribLocation() あるいは glGetAttribLocation() を用います.
gl_Position は GLSL の組み込み変数で, この変数に代入した値がパイプラインの次段 (ラスタライザあるいはジオメトリシェーダ) に送られます. バーテックスシェーダは必ずこの変数に値を代入しなければなりません. このプログラムの例では, attribute 変数 pv をそのまま gl_Position に代入していますから, バーテックスシェーダでは入力された頂点属性をそのまま座標値として次段に送ります.
フラグメントシェーダのソースプログラム
#version 150 core out vec4 fc; void main(void) { fc = vec4(1.0, 0.0, 0.0, 0.0); }
fc はフラグメントシェーダから出力するカラーデータを受け取る変数です. out はこのシェーダプログラムから次段に出力される変数であることを示します. ラスタライザは走査変換によって図形中の画素を選択し, その画素に対してフラグメントシェーダを起動します. フラグメントシェーダによってこの変数に代入された値は, ラスタ処理の結果にしたがってフレームバッファのカラーバッファに格納されます. この変数を使用中のフレームバッファのカラーバッファと対応づけるには, glBindFragDataLocation() を用います. もしフレームバッファに何も出力しないなら, ここで discard 命令を実行します.
伝統的なソフトウェアのパイプライン
実際のソフトウェアのパイプラインは, 前述のものより多少複雑になります. 下図は伝統的なソフトウェアのパイプラインの例ですが, このようなパイプラインは 1970〜1980 年くらいには完成していたと思います.
このパイプラインでは, 物体は部品ごとの座標系であるローカル座標系で定義されます. これは局所座標系あるいはモデル座標系とも呼ばれます. 個々の部品を位置や回転角,スケールなどの配置情報をもとにワールド座標系上に配置します. これをモデル変換といいます. これはモデリング変換とも呼ばれます. またワールド座標系は, グローバル座標系とも呼ばれます.
すべての部品をワールド座標系に配置してシーンの構成が完了したら, 視点情報をもとに, それを視点 (カメラ) から見たときの座標系である視点座標系に変換します. これをビュー変換といいます. これはビューイング変換あるいは視野変換とも呼ばれます.
照明による反射光強度の計算, いわゆる陰影付け (shading) は, 通常はここで個々の頂点に対して行います. 頂点の陰影付けはこのように視点座標系で行うのが一般的だと思いますが, ワールド座標系やローカル座標系で行うこともあります.
最後に, 投影情報を使って視点座標系からクリッピング座標系への変換を行います. これを投影変換といいます. これはプロジェクション変換と呼ばれることもあります. 投影変換には直交投影や透視投影が用いられます. クリッピング座標系は一定の大きさ (通常 -1 ≤ x, y, z ≤ 1) を持つクリッピング空間の座標系で, この空間からはみ出た図形はラスタ処理の際に切り取られます.
ジオメトリ処理は以上の段階に分けることができるので, これをジオメトリパイプラインと呼ぶことがあります.
ラスタ化処理では, まずクリッピング空間からはみ出た図形を切り取ります. この処理をクリッピングといいます. そして, この結果を画面上の表示領域であるビューポートにはめ込むために, ビューポート変換という座標変換を行います. これはスクリーンマッピングとも呼ばれます.
次に, ビューポートにはめ込まれた図形に対して走査変換を行い, その図形に含まれる画面上の画素を選択します. また, 頂点に与えられた頂点属性をその画素の位置における補間値を求めます. これらをもとに, その画素に対してフラグメント処理を実行します.
フラグメント処理では, 頂点属性の補間値やテクスチャを参照して, その画素の色情報を求めます (画素の陰影付け). そしてラスタ処理によってフレームバッファ上に画素のデータを合成し, 結果の画像を得ます.
GPU のハードウェアのパイプラインの概略
前述のソフトウェアのパイプラインを実現するために, GPU のハードウェアのパイプラインは, 次のような構成になっています. これは OpenGL 3.x のハードウェアのパイプラインの概念を非常に簡略化したものです. また OpenGL 4.x ではさらに要素が増えています.
頂点属性は, そのまま使う場合の他に, 頂点番号を使って間接的に参照する場合もあります. これらバッファオブジェクトに転送して使います. これはインデックスバッファオブジェクトといいます. また前述のように, 映像を生成する際には頂点属性の他に様々な情報 (パラメータ) が必要になります. 視点や光源の位置はベクトルで表された座標値ですが, モデル変換やビュー変換, 投影変換は行列で表されます. これらもあらかじめ GPU に送っておく必要があります. これらは描画命令の実行中には変更されない変数で, シェーダプログラムからは uniform 変数として参照されます. テクスチャも同様に, あらかじめ GPU に送ります.
なお, これらは個別に CPU から GPU に送られますが, バッファオブジェクトを介する場合もあります. バッファオブジェクトを介することにより, これらの管理も GPU 上で行えます. また, バッファオブジェクトは GPU 上で計算結果の保存先としても使われます. これにより複雑な計算をデータを, CPU に戻すことなく, GPU で完結して行うことができるようになります.
なお, ビューポートはラスタ化処理で参照するパラメータなので, これらとは別扱いになっています.
パイプラインというからには OpenGL4.0 から入った Program Pipeline Object(GL_ARB_separate_shader_objects) については記述しないんですか
通りすがり様,コメントありがとうございます。<br>そこまでキャッチアップできておらず,お恥ずかしい限りです。ここではグラフィックス表示の手順としてのパイプライン(しかも3.2)を取り上げております。シェーダコンテナの各パイプラインステージをユーザが個別に構成可能であるという機能は,ここの話題とは微妙に次元の違う話のように思われ,ここでうまくまとめられるほどの知識も持ち合わせておりません。ご容赦くださいますでしょうか。