2009年6月16日火曜日

OpenGL3.0、GLSL1.3の超超入門

こちらのブログが超超参考になります。 超超参考にしました。
http://monsho.blog63.fc2.com/?q=OpenGL

お日柄もよろしく、OpenGLもいつの間にかメジャーバージョンが3になりまして、仕様書をよく読んでみると1.5ぐらいの頃とはまるで違う関数名に驚きまくりの日々を送る今日この頃です。 皆様いかがお過ごしでしょうか。

少し取り乱しました。 今さっき三角形ポリゴンがやっとこさ表示できたので、ちょっとテンションがおかしくなってます。 以下、大まかなポリゴン表示までの最低限の流れと、注意点などを記述していきます。 対象としては、ちょっとぐらいならOpenGLを触ったことのある人、近年のシェーダによる3Dグラフィックス描画モデルを大体理解している人、あたりを想定しています。

OpenGL3.0の怖いところは、旧来の例えばglVertexなどの『とりあえず表示できるか試してみようや』系の関数などがまるごとDeprecated扱いされている点です。 実際の所はただDeprecatedと言われているだけで、使おうと思えば使えるのですが、この記事の主旨としてはそういったDeprecatedな関数を使わない方向で行きます。
そしてもう一つ怖いところは、これはDirect3D10でも同様なのですが、固定機能が削除されたことにより、シェーダを書かないことにはどうにもならないということです。 新しいAPIを習得するのに加え、新しい言語まで……ということで、かなーりハードルが高くなっています。
GLUTによるウィンドウの表示部分は全く変更する必要がないのが、まぁ救いと言えば救いでしょうか。

さて、まずはライブラリのインストールから始まります。 私事ですが、最近日替わりでOSを入れ替えるようにしていて、偶数日はWindows Vista、奇数日はUbuntu 9.04(Linux)と、まぁ比較的チキンなOSの選択ではあるのですがとにかくWindows環境とLinux環境を交互に使っています。 なので、ここでは両環境でのインストール周りの問題について記述します。

まずWindowsですが、OpenGLの開発をやったことのある皆様ならご存じの通り、PlatformSDKのGL/gl.hはバージョン1.1のまま停滞しています。 そのため、GLEWというライブラリを導入する必要があります。 OpenGL3.0の機能を使うためには、GLEW 1.5.1以上のバージョンが必要です。 多分、これのインストールについては困ることはほとんどないと思います。 glewInit()関数を適当なタイミングで呼び出すことさえ忘れなければ、すぐにでもOpenGL3.0の機能が使えます。 もしも実行時に0x00000000を呼び出した系のエラーが出た場合は、glewInit()の呼び出しタイミングを変えてみましょう。

で、Linuxなのですが、ややこしいことにUbuntu 9.04で入っているGLEWは1.5.0のためOpenGL3.0が使えません。しかし、デフォルトで入っているGL/gl.hが3.0のため、Ubuntu 9.04ではむしろGLEWを使う必要がありません(もちろん、拡張機能を使いたい場合は除く)。 ざっと調べてみたところ、次期Ubuntuの9.10ではGLEW1.5.1のパッケージが期待できるようです。 どーしてもUbuntu 9.04でGLEW1.5.1を使いたい人は、Makefileの改行コードをCRLF->LFに変換し、make時に見つからないと文句を言われたlibなんとかをapt-getで導入してください。 多分コンパイルが通るはずです。

それとインストールとは少し違いますが、OpenGL3.0とGLSL1.3の仕様書は手元に置いておきましょう。 適当にググれば公式っぽい所から落とせるはずです(英語が苦手な人は、Specificationの文字を頼りに!)。

さて、やっとコードを書き書きする作業に入ります。 まず、ウィンドウの表示まではGLUTに任せてしまいましょう。 GLUTの扱いに関しては、日本語の資料がWeb上に十分すぎるほど転がっているのでここでは割愛します。

ここから本番です。 大まかに、ポリゴンを表示するまでのステップは、
  • 頂点データの準備
  • シェーダコードを書く
  • シェーダをコンパイルするコードを書く
  • 頂点データをシェーダにAttachする
  • フレームごとに描画をする
という5段階に分かれます。

まず、頂点データの準備から行きましょう。 ここは比較的簡単で、適当にstructを自分で定義してやれば済む話です。 今回は簡単なポリゴン板の表示ということで、struct内には座標と色ぐらいがあれば十分でしょう。

struct vertex {
GLfloat pos[3]; // [3]でも[4]でも、何なら[2]でも大丈夫です
GLfloat color[4];
};


次に、シェーダコードを書きます。 ここでシェーダ言語の詳説をするのはとてもめんどくさいので、詳しい仕様は仕様書見ろ! ということで、最小限のコードだけ書いてみます。

頂点シェーダ

===
#version 130

in vec4 pos;
in vec4 color;

out vec4 gl_Position;
out vec4 vo_color;

void main(void) {
gl_Position = pos;
vo_color = color;
}
===


フラグメントシェーダ

===
#version 130

in vec4 vo_color;

out vec4 fragment_color;

void main(void) {
fragment_color = vo_color;
}
===


まず、一番最初に#version 130と書いてバージョンを指定してやる必要があるみたいです。 その後は、ほとんどCの構文と同じになります(void mainが気持ち悪いですが)。

シェーダへの入出力は、グローバル空間にinまたはout修飾子をつけた変数を宣言することにより定義されます。 大事なことは、
  • gl_Positionは頂点シェーダの出力として予約されている名前で、頂点の座標を示す
  • 他の名前は、頂点シェーダ→フラグメントシェーダへと渡され、それぞれの出力と入力で同じ名前でなければならない
  • フラグメントシェーダの一番最初の出力が、最終的な出力色になる
あたりでしょうか。

次に、シェーダをコンパイルします。 シェーダ周りのOpenGLのデータ構造は2つあって、ShaderとProgramというものです。 Shaderは先程書いたシェーダコード一つ一つに対応し、Programは頂点シェーダ→(ジオメトリシェーダ)→フラグメントシェーダの流れに対応します。 Shader∈Programということですね。
頂点シェーダとフラグメントシェーダの作成手順はだいたい同じで、こんな感じになります。
  • CreateShader(GL_VERTEX_SHADER)でシェーダを作成する
  • ShaderSource(shader, count, string, length)でシェーダにソースコードを読み込ませる(ちなみに、複数の文字列を渡せるようになっていますが、全部連結されます。 サイズの判らないファイルから読み込んだりするときに便利)
  • CompileShader(shader)でコンパイルする
で、これをProgramにまとめるわけです。
  • CreateProgram()でProgramを作成する
  • AttatchShader(program, shader)を何回か呼び出して、シェーダを関連づける
  • LinkProgram(program)で、シェーダ間のデータ転送がちゃんとできてるか確認する
  • UseProgram(program)で、Programが描画時に使われるようにする
と、これでProgramの作成は終わりです。

次のステップは、頂点データをProgramにアタッチする作業です。 先程のリンクがうまくいっていれば、頂点シェーダでin修飾子をつけて書いたグローバル変数が、Programへの入力として認識されているはずです。 ハマりやすいポイントなのですが、最適化の結果としてProgramへの入力が実際の頂点シェーダのコードより減っている可能性があります。 使われていない入力、0.0が掛けられている入力などは、無かったことにされてしまっているので注意しましょう。

関数の呼び出し順としては、次のようになります。
  • (任意)GetProgramiv(program, GL_ACTIVE_ATTRIBUTES, params)で、入力のデータ数を取得する
  • GetAttribLocation(program, name)で、nameという名前の入力に対応する番号を取得する
  • EnableVertexAttribArray(index)で、この番号の入力をEnable状態にする
  • VertexAttribPointer(index, size, type, normalized, stride, pointer)で、頂点データの配列をある入力番号にバインドする
strideを適切に指定し、offsetofマクロをうまいこと使えば、Direct3Dでやっているような頂点1つごとに構造体としてメモリ上にまとまっているような形式でも、ちゃんと渡すことができます。

最後のステップとして、フレームごとに(GLUTなら、glutDisplayFuncに登録した関数の呼び出しごとに)DrawArraysを呼び出して(頂点インデックスを用いない場合……用いる場合はDrawElements系を使いましょう)、Flushしてやれば完璧です。 一つだけ私が引っかかった罠を書いておきますと、DrawArraysの第三引数countは頂点の数で、決して決してプリミティブの数ではありません。 Direct3D10のID3D10Device::Draw関数でも全く同じ罠に盛大に引っかかったような記憶があります。

OpenGLはデフォルトではカリングはしない設定になっており、またzカリングも[0.0, 1.0]の範囲に入れておけばまぁ安全でしょう。 OpenGLの関数はエラー値を返さず、いちいちGetErrorでエラーを確認する必要がありますので、適当にマクロ化なり関数化しておくと便利です。 また、シェーダのコンパイル・リンク時にはさらに詳細なエラーメッセージが得られるので、GetShaderInfoLog関数で得られるエラーメッセージをちゃんと表示しておくようにするとデバッグが早いです。