Shaders
ようこそ、グレートな3Dワールドへ!
ここまで読んできていればシェーダーがどんなものかということはイメージできているでしょう。今一度まとめてみましょう。
- シェーダーは GLSL もしくは GLSL ES を使っている。
- シェーダーはいつも頂点シェーダー(VSH) とフラグメントシェーダー(FSH) のペアではたらく。
- glDrawArrays() や glDrawElements() といった描画コマンドを送るごとにシェーダーのペアは処理を実行する。
- VSH は頂点の数だけ処理を行う。
- FSH はオブジェクトの可視断片のそれぞれについて処理を実行する。FSH はグラフィックパイプラインにおいて Fragment Operations の前に処理されるので、その時点では OpenGL はどのオブジェクトが前面にあるかといった情報は知らない。つまり、隠れているようなオブジェクトも処理するということ。FSH の仕事は断片の最終的な色を定義すること。
- VSH と FSH は別々にコンパイルされ、Program Object としてリンクされなければならない。コンパイルされたシェーダーは複数の Program Object に再利用することができる。ただしそれぞれの Program Object にリンクできるのは1つのシェーダーペアである。
Shader and Program Creation
では シェーダーオブジェクトについて見てみよう。他の OpenGL のオブジェクトと同様にまずは名前/IDsを付け、設定を行う。他のと違うのは「コンパイルする」という手順があるということ。シェーダーは GPU によって処理され、OpenGL に最適なバイナリフォーマットにコンパイルされる。
シェーダーの作成に関連する関数は以下のとおり。
GLuint glCreateShader(GLenum type) GLvoid glShaderSource(GLuint shader, GLsizei count, const GLchar** string, const GLint* length) GLvoid glCompileShader(GLuint shader) // 引数の説明は省略
名前/IDsをつけて、ソースコードを渡して、それをコンパイルする。他のコードをすでにもっているシェーダーにソースコードを渡すと元のコードは破棄される。一度コンパイルしたら処理を変えることはできない。
それぞれのシェーダーはコンパイルされたかどうかのフラグを持つ。適正にコンパイルされラバ TRUE となるので、デバッグの時にはチェックすべき箇所です。加えてログを取得するのもよいでしょう。glGetShaderiv() で状態を、glGetShaderInfoLog()で状態メッセージを取得できます。
シェーダーの名前/IDを1つのリストで管理することはようにしましょう。例えば[1]という名前/IDをもつ VSH を生成して、次に作るシェーダーの名前はおそらく[2]になるでしょうし、そのあとは[3], [4], [5],,, となっていき、名前/IDが重複することはなくなります。
シェーダーペアを作ったら、次はそれらを格納する Program Object を作ります。手順としては Program Object を生成し、何かしら(ここではシェーダーのペア)をアップロードし、"link" する。このリンクによってシェーダーペア同士をつなげ、それ自体を OpenGL のコア部分にリンクさせる。はい、ここ重要です。なぜならこの過程でシェーダーに多くの検証がなされるから。シェーダーと同じように Program Object でもリンク状態とそのログを見ることができ、そこからチェックを行うべきです。リンクが成功すればそれらは正確に動くでしょう。Probram Object の関数は以下のとおり。
GLuint glCreateProgram(void) GLvoid glAttachShader(GLuint program, GLuint shader) GLvoid glLinkProgram(GLuint program)
glCreateProgram() は引数を持たない。なぜなら Program Object は一種類しか存在しないから。
シェーダーの名前/IDを単一のリストで管理する、というのは覚えているかな?OpenGL はそれぞれに特有な名前/IDによってシェーダーのタイプを得しています。よって、重要なのは glAttachShader() を2回(VSH + FSH)呼ぶということ。VSH を2つアタッチしたり、3つ以上のシェーダーをアタッチしたりするとちゃんとリンクされない。
(補足:以下、Program Object によってシェーダーペアがリンクされたオブジェクトを「プログラム」と呼んでいるみたい。)
僕らはたくさんのプログラムを作ることができるが、OpenGL はどのプログラムを使うのか?OpenGL はプログラムオブジェクトのためのアームとフックは持っていない。じゃあどうやってそれを知るのか?実はここだけ例外的な処理方法であり、バインドの関数ではないが以下のメソッドを使うことによってフックと同じように使うことができる。
GLvoid glUseProgram(GLuint program)
この関数以降に書かれた glDraw*() メソッドではこのプログラムが使われることになる。glUseProgram(0) で現在のプログラムを解除する。
では実際のコードを見てみよう。
GLuint _program; GLuint createShader(GLenum type, const char **source) { GLuint = glCreateShader(type); glShaderSource(name, 1, &source, NULL); glCompileShader(name); // "DEBUG"はデバッグのためのマクロ #if defined(DEBUG) GLint logLength; // GL_INFO_LOG_LENGTH を使う代わりに、COMPILE_STATUS も使える。 // 僕は前者が好き。もしコンパイル成功なら長さはゼロになり、 // もしエラーがでたらどのみち GL_INFO_LOG_LENGTH は必要なので。 glGetShaderiv(name, GL_INFO_LOG_LENGTH, &logLength); if (logLength > 0) { GLchar *log = (GLchar *)malloc(logLength); glGetShaderInfoLog(name, logLength, &logLength, log); // Shows the message in console. printf("%s",log); free(log); } #endif return name; } GLuint createProgram(GLuint vertexShader, GLuint fragmentShader) { GLuint name; // プログラムの名前/IDを作成 name = glCreateProgram(); // VSH と FSH を接続 glAttachShader(name, vertexShader); glAttachShader(name, fragmentShader); // それらを OpenGL のコアへリンクさせる glLinkProgram(_name); #if defined(DEBUG) GLint logLength; // この関数はシェーダーのとはちょっと違う。 glGetProgramiv(name, GL_INFO_LOG_LENGTH, &logLength); if (logLength > 0) { GLchar *log = (GLchar *)malloc(logLength); glGetProgramInfoLog(name, logLength, &logLength, log); printf("%s",log); free(log); } #endif return name; } void initProgramAndShaders() { const char *vshSource = "... Vertex Shader source using SL ..."; const char *fshSource = "... Fragment Shader source using SL ..."; GLuint vsh, fsh; vsh = createShader(GL_VERTEX_SHADER, &vshSource); fsh = createShader(GL_FRAGMENT_SHADER, &fshSource); _program = createProgram(vsh, fsh); // シェーダーの廃棄 // シェーダーはコンパイルされてしまええば OpenGL 内に保持される。 glDeleteShader(vsh); glDeleteShader(fsh); // ここでこのプログラムを使うために _program 変数をつかえるようになった。 // もしオブジェクト指向なプログラムとしてつくっているのなら // ここでクラスのインスタンス変数などに格納すること。 // glUseProgram(_program); }
OpenGL オブジェクトを作る関数から独立させるような形でテンプレートを作るとだいたい上のようになる。使うシェーダーとそのプログラムだけ変更すれば良い。
ここまではシェーダーとそのプログラムの作り方ここからは Shader Language (SL)、とくに GLSL ES について見ていこう。
Shader Language
SL はC言語によく似ている。変数宣言と関数定義の書き方は同じだし、if-else やらループ文の書き方も同じでマクロも書ける。SL は可能な限り最速になるように作られている。よってループや条件分岐の使用はコストが高いので注意が必要である。シェーダーは GPU によって処理され、浮動小数点での計算が最適化されているということを覚えておこう。この処理のため、SL は3D世界で使うための独特なデータ型をもつ。
(リスト省略/> 元ネタサイトへ)
vec 系のベクターデータはドット演算子で、もしくは[ ]演算子でそれぞれの要素にアクセスすることができる(アクセサ)。{x, y, z, w} などの表記が要素を表している。例えば、aaa.xyz はve4 型の aaa の中のはじめの3つの状態変数を表すことができる。この要素表記の順序を変えることもできる。例えば aaa.yzx というのもOK。また、{x, y, z} と {r, g, b} と同時に持ったようなものもある。重要なのはこれらをミックスもできる、ということ。例えば aaa.xrt という表記も可能。
vec4 myVec4 = vec4(0.0, 1.0, 2.0, 3.0); vec3 myVec3; vec2 myVec2; myVec3 = myVec4.xyz; // myVec3 = {0.0, 1.0, 2.0}; myVec3 = myVec4.zzx; // myVec3 = {2.0, 2.0, 0.0}; myVec2 = myVec4.bg; // myVec2 = {2.0, 1.0}; myVec4.xw = myVec2; // myVec4 = {2.0, 1.0, 2.0, 1.0}; myVec4[1] = 5.0; // myVec4 = {2.0, 5.0, 2.0, 1.0};
次は変換そ見てみよう。これについてはいくつか注意が必要。SL は値の最小値と最大値を定義して値の幅を定めており、これを Precision Qualifiers という。
変数宣言で使うことのできるキーワードは多くない。以下がそのリストになるが、これはメーカーによって桁数が増える場合がある。よって下のリストは SL に必要な最小レンジである。
(表省略/> 元ネタサイトへ)
これらの修飾子を型宣言で使う代わりに、"precision" というキーワードを使って修飾子のデフォルトを設定することもできる。また、あまりよくないことではあるが、これらの修飾子を使って異なるデータを変換できる。例えば小数点型から整数型に変換するために、 mediump float と lowp int を使うべきであり、lowp float から変換すると -2.0 〜 2.0 にまるめられてしまうので注意しよう。埋め込み関数も使える。
precision mediump float; precision lowp int; vec4 myVec4 = vec4(0.0, 1.0, 2.0, 3.0); ivec3 myIvec3; mediump ivec2 myIvec2; // ↓はデータ型の連携がないのでダメ //myIvec3 = myVec4.zyx; myIvec3 = ivec3(myVec4.zyx); // This is OK. myIvec2.x = myIvec3.y; // This is OK. myIvec2.y = 1024; // ↓は大丈夫ではあるが、値の型の正確性が異なっているので // myIvec3.x の値は型の範囲制限で256にまるめられる。 myIvec3.x = myIvec2.y;
GPU で直接に処理することによるもう1つの利点は浮動小数点の計算である。掛け算やその他の計算をごく簡単に実行することができる。行列、ベクター、小数点型は完全に互換性があり、いろいろできる。以下は例。
mat4 myMat4; mat3 myMat3; vec4 myVec4 = vec4(0.0, 1.0, 2.0, 3.0); vec3 myVec3 = vec3(-1.0, -2.0, -3.0); float myFloat = 2.0; // mat4 は16の要素を持ち、4つの vec4 で初期化できる。 myMat4 = mat4(myVec4,myVec4,myVec4,myVec4); // それぞれの要素に掛け算 myVec4 = myFloat * myVec4; // 4x4行列と1x4行列の計算に相当。結果は vec4 型。 myVec4 = myMat4 * myVec4; // アクセサを使って異なる配置のベクターを乗算 myVec4.xyz = myVec3 * myVec4.xyz; // mat4 のはじめの9エレメントで mat3 を作成 myMat3 = mat3(myMat4); // 3x3行列と1x3行列の乗算に相当。結果は vec3 型。 myVec3 = myMat3 * myVec3;
もちろんCのように配列を使って初期化することもできる。SL はどのシェーダーも
void main()
を持つように定めている。シェーダーの処理はここから開始する。これを持たないシェーダーはコンパイルされない。SL はインラインな言語だと覚えておこう。つまりは関数の前方宣言をするべしという話。よって void main()
は大体最後になる。では 頂点シェーダー(VSH) とフラグメントシェーダー(FSH) の作るものについてもっと見てみよう。
> 2/3-4 へ