固定機能の廃止
レンダリングパイプラインにおいて固定機能ハードウェアで実装されていた機能が廃止された場合、それらをシェーダで実装しなければ図形を描くことができません。このため、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() は、例えば次のようになります。
#if defined(_WIN32)
# define _USE_MATH_DEFINES
# define _CRT_SECURE_NO_WARNINGS
# include <GL/glew.h>
# include <GL/glut.h>
#elif defined(__APPLE__)
# define GL_SILENCE_DEPRECATION
# include <GLUT/glut.h>
#else
# define GL_GLEXT_PROTOTYPES
# include <GL/glut.h>
#endif
#include <stdio.h>
#include <stdlib.h>
/*
** シェーダーのソースプログラムをメモリに読み込む
*/
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() の入っているソースファイルに以下の内容を追加してください。
#if defined(_WIN32)
# define _USE_MATH_DEFINES
# define _CRT_SECURE_NO_WARNINGS
# include <GL/glew.h>
# include <GL/glut.h>
#elif defined(__APPLE__)
# define GL_SILENCE_DEPRECATION
# include <GLUT/glut.h>
#else
# define GL_GLEXT_PROTOTYPES
# include <GL/glut.h>
#endif
#include <stdio.h>
#include <stdlib.h>
/*
** シェーダのソースプログラムの読み込みに使う関数
*/
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) を実行します。