■ 2009年08月27日 [OpenGL][GLSL][ゼミ] 第3回 シェーダプログラム
固定機能の廃止
レンダリングパイプラインにおいて固定機能ハードウェアで実装されていた機能が廃止された場合, それらをシェーダで実装しなければ図形を描くことができません. このため, OpenGL を使ってプログラムを書く際には, CG の基礎的な理論に関する知識が必須になりました. CG の授業をしていて, 内心で「でもこれはハードウェアに組み込まれてるんだよな」とか思ったりしてましたけど, 無駄ではなかったんだと胸をなでおろしていたりします.
シェーダプログラムとラスタライザ
アプリケーションプログラムは, 描画する基本図形を指定した後, 頂点情報を GPU に送ります.
頂点情報はバーテックスシェーダの attribute 変数に格納され, これを用いて座標変換や陰影計算などを行います. 頂点の座標値はクリッピング座標系に変換して, バーテックスシェーダの出力変数 gl_Position に格納します. この値をもとにクリッピングを行った結果をビューポート変換 (screen mapping) し, ラスタライザにより画素に展開します. 一方, テクスチャ座標や陰影計算により求めた頂点の色は, varying 変数に格納します. これはラスタライザにより補間され, フラグメントシェーダに送られます.
GLSL の version 1.3 以降では, 頂点情報はバーテックスシェーダの入力である in 変数に格納されます. また, バーテックスシェーダの計算結果は, varying 変数ではなく次のシェーダステージに出力する out 変数に格納します.
ラスタライザは図形要素を画素に展開し, その一つ一つに対してフラグメントシェーダを起動します. フラグメントシェーダの varying 変数には, バーテックスシェーダで設定した値の補間値が格納されています. これを使って画素の陰影の決定やテクスチャのサンプリングを行い, 最終的に決定した画素の色をフラグメントシェーダの出力変数 gl_FragColor に格納します.
GLSL の version 1.3 以降では, バーテックスシェーダから out 変数に出力した値は, 補間されてフラグメントシェーダの in 変数に格納されます. また, フラグメントシェーダから出力する画素の色は, gl_FragColor ではなくユーザ定義の out 変数に格納します.
バーテックスシェーダでは, 最低限 gl_Position に値を設定する必要があります. フラグメントシェーダでは, 最低限 gl_FragColor に値を設定するか, discard (その画素を画面に描かない) を実行する必要があります.
バーテックスシェーダ
OpenGL 2.1 を想定しているので, GLSL のバージョンに 1.2 を指定します (#version 120). バーテックスシェーダでは, 頂点の位置を格納する (予定の) attribute 変数 position を, そのまま gl_Position に代入します. position は, とりあえず2次元 (vec2) とします. gl_Position は4次元 (vec4) なので, vec4() を使って2次元から4次元に変換します. このとき z = 0.0 とし, 同次座標なので w = 1.0 とします. なお gl_Position 変数は, 異なる (独立にコンパイルされた) バーテックスシェーダの間で同一のものを示しています. そのため, この変数に代入する式がそれらのシェーダで同じであり, その式に入力される値も同じなら, この変数は, invariant として宣言します. このファイル名は simple.vert とします.
#version 120 // // simple.vert // invariant gl_Position; attribute vec2 position; void main(void) { gl_Position = vec4(position, 0.0, 1.0); }
フラグメントシェーダ
フラグメントシェーダでも, GLSL のバージョンに 1.2 を指定します. バーテックスシェーダからは gl_Position 以外何も出力されていない (varying 変数を定義していない) ので, 画素の色として定数 (ここでは赤色) を出力します. gl_FragColor も vec4 (r, g, b, a, a はアルファ値) なので, vec4() を使います. このファイル名は simple.frag とします.
#version 120 // // simple.frag // void main(void) { gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); }
シェーダプログラムの読み込み
GLSL のシェーダプログラムを利用する手順は, 以下のようになります.
- バーテックスシェーダとフラグメントシェーダのシェーダオブジェクトを作成します (glCreateShader()).
- 作成したそれぞれのシェーダオブジェクトに対してソースプログラムを読み込みます (glShaderSource()).
- 読み込んだソースプログラムをコンパイルします (glCompileShader()).
- プログラムオブジェクトを作成します (glCreateProgram()).
- プログラムオブジェクトに対してシェーダオブジェクトを登録します (glAttachShader()).
- シェーダプログラムをリンクします (glLinkProgram()).
- シェーダプログラムを適用します (glUseProgram()).
まず, シェーダのソースプログラムを読み込む関数 readShaderSource() を考えます. シェーダのソースプログラムをドライバに読み込む関数 glShaderSource() の仕様を以下に示します.
- void glShaderSource(GLuint shader, GLsizei count, const GLchar **string, const GLint *length );
- シェーダのソースプログラムを読み込みます. shader は glCreateShader() の返り値として得られるシェーダオブジェクトの名前 (番号) です. string にシェーダのソースプログラムを格納します. これは GLchar 型の配列へのポインタの配列です. count は string の要素数です. length が NULL の時は, string の各要素が指す配列の終端は '\0' になっている必要があります.
したがってシェーダのソースプログラムは, 次のようにしてアプリケーションのソースプログラムに埋め込むことができます.
... static GLchar *source[] = { "#version 120", "void main(void)", "{", " gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);", "}", }; ... glShaderSource(shader, sizeof source / sizeof source[0], source, NULL); ...
ファイルに書いたシェーダのソースプログラムを読み込む場合は, ファイルのサイズを調べて length に指定し, count は 1 にすればいいでしょう. この場合 readShaderSource() は, 例えば次のようになります.
#include <stdio.h> #include <stdlib.h> #if defined(WIN32) # pragma warning(disable: 4996) # include <malloc.h> # include "glew.h" # include "glut.h" # include "glext.h" #elif defined(__APPLE__) || defined(MACOSX) # include <GLUT/glut.h> #else # define GL_GLEXT_PROTOTYPES # include <GL/glut.h> #endif /* ** シェーダーのソースプログラムをメモリに読み込む */ int readShaderSource(GLuint shader, const char *file) { FILE *fp; const GLchar *source; GLsizei length; int ret; /* ファイルを開く */ fp = fopen(file, "rb"); if (fp == NULL) { perror(file); return -1; } /* ファイルの末尾に移動し現在位置 (つまりファイルサイズ) を得る */ fseek(fp, 0L, SEEK_END); length = ftell(fp); /* ファイルサイズのメモリを確保 */ source = (GLchar *)malloc(length); if (source == NULL) { fprintf(stderr, "Could not allocate read buffer.\n"); return -1; } /* ファイルを先頭から読み込む */ fseek(fp, 0L, SEEK_SET); ret = fread((void *)source, 1, length, fp) != (size_t)length; fclose(fp); /* シェーダのソースプログラムのシェーダオブジェクトへの読み込み */ if (ret) fprintf(stderr, "Could not read file: %s.\n", file); else glShaderSource(shader, 1, &source, &length); /* 確保したメモリの開放 */ free((void *)source); return ret; }
この他, シェーダのコンパイル時やリンク時のメッセージがわからないと, シェーダのデバッグができないので, これらを表示する関数を用意します. シェーダのコンパイル時のメッセージは glGetShaderiv() と glGetShaderInfoLog() を組み合わせて得られますから, これらを用いて printShaderInfoLog() という関数を定義します.
/* ** シェーダの情報を表示する */ void printShaderInfoLog(GLuint shader) { GLsizei bufSize; /* シェーダのコンパイル時のログの長さを取得する */ glGetShaderiv(shader, GL_INFO_LOG_LENGTH , &bufSize); if (bufSize > 1) { GLchar *infoLog = (GLchar *)malloc(bufSize); if (infoLog != NULL) { GLsizei length; /* シェーダのコンパイル時のログの内容を取得する */ glGetShaderInfoLog(shader, bufSize, &length, infoLog); fprintf(stderr, "InfoLog:\n%s\n\n", infoLog); free(infoLog); } else fprintf(stderr, "Could not allocate InfoLog buffer.\n"); } }
- void glGetShaderiv(GLuint shader, GLenum pname, GLint *params);
- シェーダオブジェクトのパラメータを取り出します. shader はシェーダオブジェクトの名前です. pname は取り出すパラメータの種類です. GL_COMPILE_STATUS を指定すれば, シェーダのコンパイルの成否を取り出します. GL_INFO_LOG_LENGTH を指定すれば, シェーダのコンパイル時に作成されたログの長さを取り出します. params には取り出された値が格納されます.
- void glGetShaderInfoLog(GLuint shader, GLsizei maxLength, GLsizei *length, GLchar *infoLog);
- シェーダのコンパイル時のログ (メッセージ) を取り出します. shader はシェーダオブジェクトの名前です. maxLength には取り出すログの長さ (文字数) を指定します. これは infoLog に指定した配列のサイズを超えてはいけません. length には実際に infoLog に格納されたログの長さが格納されます. infoLog にログが格納されます.
同様にして, シェーダのリンク時のメッセージを表示する関数 printProgramInfoLog() という関数を定義します. これには glGetProgramiv() と glGetProgramInfoLog() を組み合わせて使います.
/* ** プログラムの情報を表示する */ void printProgramInfoLog(GLuint program) { GLsizei bufSize; /* シェーダのリンク時のログの長さを取得する */ glGetProgramiv(program, GL_INFO_LOG_LENGTH , &bufSize); if (bufSize > 1) { GLchar *infoLog = (GLchar *)malloc(bufSize); if (infoLog != NULL) { GLsizei length; /* シェーダのリンク時のログの内容を取得する */ glGetProgramInfoLog(program, bufSize, &length, infoLog); fprintf(stderr, "InfoLog:\n%s\n\n", infoLog); free(infoLog); } else fprintf(stderr, "Could not allocate InfoLog buffer.\n"); } }
- void glGetProgramiv(GLuint program, GLenum pname, GLint *params);
- シェーダオプログラムのパラメータを取り出します. programは glCreateProgram() の返り値として得られるシェーダプログラムの名前 (番号) です. pname は取り出すパラメータの種類です. GL_LINK_STATUS を指定すれば, シェーダのリンクの成否を取り出します. GL_INFO_LOG_LENGTH を指定すれば, シェーダのリンク時に作成されたログの長さを取り出します. params には取り出された値が格納されます.
- void glGetProgramInfoLog(GLuint program, GLsizei maxLength, GLsizei *length, GLchar *infoLog);
- シェーダのリンク時のログ (メッセージ) を取り出します. program はシェーダプログラムの名前です. maxLength には取り出すログの長さ (文字数) を指定します. これは infoLog に指定した配列のサイズを超えてはいけません. length には実際に infoLog に格納されたログの長さが格納されます. infoLog にログが格納されます.
上のソースプログラムの関数に static が付いてないってことは, これをメインプログラムのソースファイルとは別のファイルにしてねってことだから, そこんとこよろしく. ファイル名にも, ちゃんと意味のある (中身のわかる) ものを付けましょう.
オブジェクトの名前
シェーダオブジェクトとプログラムオブジェクトの名前 (番号) を格納する変数を用意します. 以下の変数のうち, 少なくとも gl2Program は描画時に使いますから, その部分から見えるような場所に置く必要があります. main() の入っているソースファイルに太字の内容を追加してください.
#include <stdio.h> #include <stdlib.h> #if defined(WIN32) //# pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"") # pragma comment(lib, "glew32.lib") # include "glew.h" # include "glut.h" # include "glext.h" #elif defined(__APPLE__) || defined(MACOSX) # include <GLUT/glut.h> #else # define GL_GLEXT_PROTOTYPES # include <GL/glut.h> #endif /* ** シェーダのソースプログラムの読み込みに使う関数 */ extern int readShaderSource(GLuint shader, const char *file); extern void printShaderInfoLog(GLuint shader); extern void printProgramInfoLog(GLuint program); /* ** シェーダオブジェクト */ static GLuint vertShader; static GLuint fragShader; static GLuint gl2Program; ...
また, シェーダプログラムのコンパイルとリンクの結果を取り出す変数も用意します.
... /* ** 初期化 */ static void init(void) { /* シェーダプログラムのコンパイル/リンク結果を得る変数 */ GLint compiled, linked; #if defined(WIN32) /* GLEW の初期化 */ GLenum err = glewInit(); if (err != GLEW_OK) { fprintf(stderr, "Error: %s\n", glewGetErrorString(err)); exit(1); } #endif
シェーダオブジェクトの作成
glCreateShader() を使ってシェーダオブジェクトを作成し, その識別子を得ます. そして, それらの識別子に対してシェーダのソースプログラムを読み込みます. この読み込みには前に作成した readShaderSource() を使います. 関数 init() の最後の部分に, 以下の内容を追加します.
/* 背景色 */ glClearColor(1.0, 1.0, 1.0, 1.0); /* シェーダオブジェクトの作成 */ vertShader = glCreateShader(GL_VERTEX_SHADER); fragShader = glCreateShader(GL_FRAGMENT_SHADER); /* シェーダのソースプログラムの読み込み */ if (readShaderSource(vertShader, "simple.vert")) exit(1); if (readShaderSource(fragShader, "simple.frag")) exit(1);
- GLuint glCreateShader(GLenum shaderType);
- シェーダオブジェクトを作成します. shaderType には, バーテックスシェーダを作成する場合は GL_VERTEX_SHADER, フラグメントシェーダを作成する場合には GL_FRAGMENT_SHADER を指定します.
読み込んだシェーダのソースプログラムを, glCompileShader() を使ってコンパイルします. コンパイルが成功したかどうかは, glGetShaderiv() を使って変数 compiled に得ます. この内容が GL_FALSE なら, コンパイルに失敗したことになります. コンパイル時に出力されたメッセージを, 前に作成した printShaderInfoLog() を使って標準エラー出力に出力します.
/* バーテックスシェーダのソースプログラムのコンパイル */ glCompileShader(vertShader); glGetShaderiv(vertShader, GL_COMPILE_STATUS, &compiled); printShaderInfoLog(vertShader); if (compiled == GL_FALSE) { fprintf(stderr, "Compile error in vertex shader.\n"); exit(1); } /* フラグメントシェーダのソースプログラムのコンパイル */ glCompileShader(fragShader); glGetShaderiv(fragShader, GL_COMPILE_STATUS, &compiled); printShaderInfoLog(fragShader); if (compiled == GL_FALSE) { fprintf(stderr, "Compile error in fragment shader.\n"); exit(1); }
- void glCompileShader(GLuint shader);
- シェーダのソースプログラムをコンパイルします. shader はシェーダオブジェクトの名前です.
プログラムオブジェクトの作成
シェーダのソースプログラムのコンパイルに成功したら, プログラムオブジェクトを作成し, シェーダオブジェクトを登録します. この時点でシェーダオブジェクトは不要になるので, 削除してしまいます.
/* プログラムオブジェクトの作成 */ gl2Program = glCreateProgram(); /* シェーダオブジェクトのシェーダプログラムへの登録 */ glAttachShader(gl2Program, vertShader); glAttachShader(gl2Program, fragShader); /* シェーダオブジェクトの削除 */ glDeleteShader(vertShader); glDeleteShader(fragShader);
- GLuint glCreateProgram(void);
- シェーダプログラムを作成します.
- void glAttachShader(GLuint program, GLuint shader);
- シェーダプログラムにシェーダオブジェクトを結合します. program はシェーダプログラムの名前です. shader は program に結合するシェーダオブジェクトの名前です.
- void glDeleteShader(GLuint shader);
- シェーダオブジェクトを削除します. shader は削除するシェーダオブジェクトの名前です.
attribute 変数の index の指定
attribute 変数 position には, アプリケーションプログラム側から頂点情報 (位置) を与えなければならないので, この変数の index を獲得します. これには, 以下の二つの方法があります.
- index の決定をシェーダのリンカ glLinkProgram() に任せて, リンク後に glGetAttribLocation() でそれを調べる方法
- リンク前に glBindAttribLocation() を使って index を明示的に指定する方法
ここでは二つ目の方法を採用します.
/* attribute 変数 position の index を 0 に指定する */ glBindAttribLocation(gl2Program, 0, "position");
- void glBindAttribLocation(GLuint program, GLuint index, const GLchar *name);
- attribute 変数の index を指定します. program はシェーダプログラムの名前です. index に指定できる値は, 0 〜 MAX_VERTEX_ATTRIBS - 1 までです. MAX_VERTEX_ATTRIBS はハードウェアに依存した値で, 少なくとも 16 です. 実際の値は glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &n); として n に得ることができます. name は index を指定する attribute 変数の名前です.
プログラムオブジェクトのリンク
glLinkProgram() によってシェーダプログラムをリンクします. リンクが成功したかどうかは, glGetProgramiv() を使って変数 linked に得ます. この内容が GL_FALSE なら, リンクに失敗したことになります. シェーダのリンク時に出力されたメッセージを, 前に作成した printProgramInfoLog() を使って標準エラー出力に出力します.
/* シェーダプログラムのリンク */ glLinkProgram(gl2Program); glGetProgramiv(gl2Program, GL_LINK_STATUS, &linked); printProgramInfoLog(gl2Program); if (linked == GL_FALSE) { fprintf(stderr, "Link error.\n"); exit(1); } } ...
- void glLinkProgram(GLuint program);
- シェーダオブジェクトをリンクして, シェーダプログラムを完成させます. program はシェーダプログラムの名前です.
これでシェーダプログラムの読み込みは完了しました. このシェーダプログラムを使って図形を描画するには, 描画の前に glUseProgram(gl2Program) を実行します.
いつも勉強させて頂いております.<br><br>OpenGLで固定パイプラインが非推奨になったということで,今風のOpenGLの勉強をしております.<br><br>そこで質問なのですが,GLSLで固定パイプラインと同等の描画を再現するにはどうすれば良いでしょうか?<br>Texture, Material(Ambient, Diffuse等)や複数の光源位置などを使った従来のOpenGL(固定パイプライン)の描画を,今風(GLSL)で再現したいのです.<br><br>どこかにサンプルがあると良いのですが.<br><br><br>よろしくお願い致します.
鴨居様、<br><br>コメントありがとうございます。お返事が遅くなり、申し訳ありません。<br><br>> OpenGLで固定パイプラインが非推奨になった<br><br>実際には、「OpenGL のバージョン 3.0 以降」では非推奨となったので、ドライバがサポートしていれば、バージョン 2.1 のコンテキストを使用することで、固定機能パイプラインも使用できます。<br><br>> GLSLで固定パイプラインと同等の描画を再現するにはどうすれば良いでしょうか?<br><br>実は以前、同じことを考えたことがありまして、OpenGL の固定機能パイプラインと同じ機能を GLSL で実装しようとしたこともあります。この日記のこの記事以降も、そういう感じで書いていました。<br><br>でも、そのうち、それは本当に必要なのかなと思うようになりました。固定機能パイプラインではもうアプリケーション側の要求に対応できないからプログラムシェーダが導入されたわけですから、それをわざわざ固定機能パイプラインの制限に押し込めることは、本当に意味のあることなのかなと。<br><br>また、GLUT では難しいですが、GLFW などでは OpenGL 2.1 のコンテキストを指定することができますから、わざわざ GLSL で固定機能パイプラインをエミュレーションする必要はないようにも思えます。それに固定機能パイプラインを制御するために OpenGL の API を羅列するより、GLSL で書いた方がスッキリと簡単に書けることも多いです。<br><br>> Texture, Material(Ambient, Diffuse等)や複数の光源位置などを使った<br>> 従来のOpenGL(固定パイプライン)の描画<br><br>この日記のこの記事以降でも、これらに関して簡単に説明しています。GLSL を使ってプログラムを書くためには、固定機能パイプラインで使われている CG の理論を自分で実装するだけの話です。<br><br>しかし、それに手間がかかるのも事実です。であれば、固定機能パイプラインという古く機能の制限されたものをわざわざ再現するより、より新しい考え方に基づいたミドルウェアを使用する方が、現実的ではないでしょうか。<br><br>例えば、次のようなツールキットがあります。<br>openFrameworks http://openframeworks.jp/<br>Cinder http://libcinder.org/<br>OpenSceneGraph http://www.openscenegraph.org/<br><br>ゲームエンジンを使うことも考えられます。<br>Irrlicht http://irrlicht.sourceforge.net/<br>OGRE http://www.ogre3d.org/<br><br>もちろん、Unity3d や Unreal Engine などの、商用ゲームエンジンも使用できます。<br><br>いかがでしょうか。
詳しい回答ありがとうございます.<br><br>GLFWでOpenGL 2.1 のコンテキストを指定することができるのですね.<br>安心しました.<br><br>また,ツールキット,知らなかったものも教えて頂いて参考になります.<br>調べてみます.