«(3) OpenGL のバージョンとプロファイルの指定 最新 (5) パイプライン»

床井研究室

現在ここに和歌山大学外から連続してアクセスすると,2回目以降にアクセスできなくなる現象が発生しています.その場合はお手数をおかけしますが,ブラウザのキャッシュのクリアをお試しください.


■ 2012年09月09日 [OpenGL][GLFW] (4) シェーダの追加

2012年09月23日 00:22更新

補助プログラム

前回 OpenGL 3.2 の Core Profile を使うように設定してしまったので, 固定機能が使えません. そのため, シェーダプログラムを作成しなければ, 図形の描画を行うことができません.

加えて, プラットフォームによってはグラフィックスハードウェアが備えているすべての機能の API を用意していない場合があり (Windows に標準搭載されている OpenGL はバージョン 1.1 までの API しか対応していません), 足らない API を使うにはグラフィックスハードウェアのドライバが用意するエントリポイントを取り出してくる必要があります. これには GLEW という便利なライブラリがあるのですが, 私が GLFW を使い始めたときに, なぜか GLEW と GLFW の共存がうまくいかなかった (ライブラリのリンクに失敗した) ので, 自前で用意することにしました.

なお, その後に「問題なく行えている」という指摘を頂いたので, 別のマシンでもう一度やってみたらちゃんとできました. なんであのとき失敗したんだろう? ヘッダファイルの順序も問題なかったし.

この部分は本題ではないのと, Windows / Mac OS X / Linux でソースを共通化するために他にもゴニョゴニョしたかったので, この部分を gg.cpp と gg.h というファイルにまとめました. この二つのファイルをソースファイルと同じディレクトリ (フォルダ) に置き, プロジェクトに追加してください. また, これらの他に最新の glext.h もダウンロードして, ソースプログラムと同じディレクトリ (フォルダ) に入れてください.

追加するファイル

そして #include <GL/glfw.h> を #include "gg.h" と using namespace gg; の2行に置き換え, ウィンドウを開いた後で ggInit() を実行してください.

ソースプログラムの変更点

#include <iostream>
#include <cstdlib>
 
// 補助プログラム
#include "gg.h"
using namespace gg;
 
// 初期設定
static void init(void)
{
  // 背景色
  glClearColor(1.0f, 1.0f, 1.0f, 0.0f);
}
 
int main(int argc, const char * argv[])
{
  // GLFW を初期化する
  if (!glfwInit())
  {
    // 初期化に失敗した
    std::cerr << "Can't initialize GLFW." << std::endl;
    exit(EXIT_FAILURE);
  }
  
  // OpenGL Version 3.2 Core Profile を選択する
  glfwOpenWindowHint(GLFW_OPENGL_VERSION_MAJOR, 3);
  glfwOpenWindowHint(GLFW_OPENGL_VERSION_MINOR, 2);
  glfwOpenWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
  
  // GLFW のウィンドウを開く
  if (!glfwOpenWindow(0, 0, 0, 0, 0, 0, 0, 0, GLFW_WINDOW))
  {
    // ウィンドウが開けなかった
    std::cerr << "Can't open GLFW window." << std::endl;
    exit(EXIT_FAILURE);
  }
  
  // 開いたウィンドウに対する設定
  glfwSwapInterval(1);
  glfwSetWindowTitle("sample");
  
  // 補助プログラムの初期化
  ggInit();
  
  // OpenGL の初期設定
  init();
  
  // 図形を表示する
  while (glfwGetWindowParam(GLFW_OPENED))
  {
    // 画面消去
    glClear(GL_COLOR_BUFFER_BIT);
    
    /*
    ** ここで OpenGL による描画を行う
    */
    
    glfwSwapBuffers();
  }
  
  return EXIT_SUCCESS;
}

以上の設定を行ったソースファイルを, 下記に用意しています.

ちなみに, Windows / Mac OS X / Linux でソースを共通化する必要がなければ (たいていそうですが) それぞれ次のように設定すればいいんじゃないかと思います. ただ, 以降で gg.h / gg.cpp で定義している他の関数も使うかもしれないので, ここでは上記のプログラムのまま進めたいと思います.

Windows で GLEW を使う

#define GLFW_NO_GLU
#incoude <GL/glew.h>
#incoude <GL/glfw.h>
  
...
  
  // 補助プログラム
  if (glewInit() != GLEW_OK)
  {
    // エラーメッセージ等
    exit(EXIT_FAILURE);
  }

Mac OS X

#define GLFW_NO_GLU
#define GLFW_INCLUDE_GL3
#include <GL/glfw.h>
#include <OpenGL/glext.h>
#include <OpenGL/gl3ext.h>

Linux 等 (X11)

#define GLFW_NO_GLU
#define GL_GLEXT_PROTOTYPES
#include <GL/glfw.h>
#include <GL/glext.h>

シェーダの作成

GLSL のシェーダプログラムを利用する手順は, 以下のようになります.

  1. まずバーテックスシェーダおよびフラグメントシェーダ, そして必要ならジオメトリシェーダのシェーダオブジェクトを作成します (glCreateShader()).
  2. 作成したそれぞれのシェーダオブジェクトに対して, ソースプログラムを読み込みます (glShaderSource()).
  3. 読み込んだソースプログラムをコンパイルします (glCompileShader()).
  4. プログラムオブジェクトを作成します (glCreateProgram()).
  5. プログラムオブジェクトに対してシェーダオブジェクトを登録します (glAttachShader()).
  6. プログラムオブジェクトをリンクします (glLinkProgram()).
  7. プログラムオブジェクトを適用します (glUseProgram()).

シェーダオブジェクトの作成

まず, シェーダのソースプログラムを用意します. たとえばバーテックスシェーダとして, 次のようなプログラムを作成したとします.

#version 150 core
in vec4 pv;
void main(void)
{
  glPosition = pv;
}

この各行を文字列にして, 次のような配列に格納します. シェーダのソースプログラムは, このような文字列の配列になっている必要があります. シェーダのプログラミングについては, 後に解説します.

  static const char *source[] =
  {
    "#version 150 core\n",
    "in vec4 pv;",
    "void main(void)",
    "{",
    "  glPosition = pv;",
    "}",
  };

このソースプログラムからシェーダオブジェクトを作成するには, 次のようなプログラムを用います. これはバーテックスシェーダの例ですが, フラグメントシェーダとジオメトリシェーダも同じです. この GL_VERTEX_SHADER をそれぞれ GL_FRAGMENT_SHADER, GL_GEOMETRY_SHADER に書き換えるだけです.

  GLuint shader = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(shader, sizeof source / sizeof source[0], source, 0);
  glCompileShader(shader);
GLuint glCreateShader(GLenum shaderType)
空のシェーダオブジェクトを作成します. 戻り値は作成されたシェーダオブジェクトのハンドル (識別名) で, 作成できれば 0 でない正の整数を返します. 作成できなければ 0 を返します. shaderType には, バーテックスシェーダなら GL_VERTEX_SHADER, フラグメントシェーダなら GL_FRAGMENT_SHADER, ジオメトリシェーダなら GL_GEOMETRY_SHADER を指定します.
void glShaderSource(GLuint shader, GLsizei count, const GLchar **string, const GLint *length)
シェーダのソースプログラムを読み込みます. shader には glCreateShader() で得たシェーダオブジェクトのハンドルを指定します. count には string に指定した配列の要素数を指定します. string にソースプログラムを書いた文字列の配列を指定します. length には string に指定した配列の各要素の文字列の長さを格納した配列を指定します.
void glCompileShader(GLuint shader)
shader に指定したシェーダオブジェクトに読み込まれたソースファイルをコンパイルします.

glShaderSource() の引数 string に渡す配列の各要素が終端にヌル文字 ('\0') をもつ通常の文字列なら, 引数 length を 0 (NULL) にします. なお, このソースプログラムの1行目の #version の行だけは, 行末に '\n' が必要です.

ソースプログラムが単一の文字列なら, count = 1 として string に文字列のポインタのポインタを渡せばいいことになります. ただしこの書き方の場合は, 各行の行末に '\n' を入れておかないと, エラーメッセージの行番号がどれも2になってしまいます.

  static const char source[] = 
    "#version 150 core\n"
    "in vec4 pv;\n"
    "void main(void)\n"
    "{\n"
    "  glPosition = pv;\n"
    "}\n";
  
  ...
  
  glShaderSource(shader, 1, &source, 0);
  ...

引数 length が 0 (NULL) でなければ, length の各要素には string の対応する要素の文字列の長さ (文字数) を格納します. また, この length の要素に負の値を格納したときは, string の対応する要素の文字列の終端がヌル文字 ('\0') になっている必要があります.

プログラムオブジェクトの作成

glCreateProgram() で空のプログラムオブジェクトを作成し, glAttachShader() を使って使用するシェーダオブジェクトを取り付けます. プログラムオブジェクトに取り付けたシェーダオブジェクトには, glDeleteShader() で削除マークを付けておきます.

  GLuint program = glCreateProgram();
  glAttachShader(program, shader);
  glDeleteShader(shader);
  ...
GLuint glCreateProgram(void)
空のプログラムオブジェクトを作成します. 戻り値は作成されたプログラムオブジェクトのハンドル (識別名) で, 作成できれば 0 でない正の整数を返します. 作成できなければ 0 を返します.
void glAttachShader(GLuint program, GLuint shader)
program に指定したプログラムオブジェクトに shader に指定したシェーダオブジェクトを取り付けます.
void glDetachShader(GLuint program, GLuint shader)
program に指定したプログラムオブジェクトから shader に指定したシェーダオブジェクトを取り外し.
void glDeleteShader(GLuint shader)
shader に指定したシェーダオブジェクトに削除マークを付けます.
void glDeleteProgram(GLuint program)
program に指定したプログラムオブジェクトに削除マークを付けます.

glDeleteShader() で削除マークを付けたシェーダオブジェクトが実際に削除されるのは, そのシェーダオブジェクトがすべてのプログラムオブジェクトから glDetachShader() によって取り外されたときです. プログラムオブジェクトが削除されるときには, それに取り付けられているシェーダオブジェクトは自動的に取り外されます. したがって, 削除マークが付けられたシェーダオブジェクトは, 他に取り付けられているプログラムオブジェクトがなければ, そのタイミングで削除されます.

glDeleteProgram() で指定されたプログラムオブジェクトは, どのレンダリングコンテキストでも使用されなくなったときに削除されます.

プログラムオブジェクトのリンク

プログラムオブジェクトに必要なシェーダオブジェクトを取り付けたら, プログラムオブジェクトのリンクを行います. その前に, 入力された頂点のデータ (これを頂点属性 - attribute といいます) をバーテックスシェーダが参照する際に用いる変数 (attribute 変数, ここでは変数名を pv とします) の位置 (index) と, フラグメントシェーダから出力するデータを格納する変数 (ここでは変数名を fc とします) の出力先 (colorNumber) を指定しておきます. これらはいずれも 0 以上の整数です. CPU 側のホストプログラムと GPU 側のシェーダプログラムの間のデータの受け渡しは, このような番号を介して行います. これは GPU のハードウェアのレジスタ番号に相当します.

  glBindAttribLocation(program, 0, "pv");
  glBindFragDataLocation(program, 0, "fc");
  glLinkProgram(program);
void glBindAttribLocation(GLuint program, GLuint index, const GLchar *name)
program に指定したプログラムオブジェクトのバーテックスシェーダ中にある name という変数の位置を index に指定します.
void glBindFragDataLocation(GLuint program, GLuint colorNumber, const char *name)
program に指定したプログラムオブジェクトのフラグメントシェーダ中にある name という変数の出力先を colorNumber に割り当てられたカラーバッファにします. デフォルトでは, 0 は標準のフレームバッファのカラーバッファが割り当てられています.
void glLinkProgram(GLuint program)
program に指定したプログラムオブジェクトをリンクします. これが成功すれば, シェーダプログラムが完成します.

プログラムオブジェクトを作成する関数

以上の手続きを関数にまとめると, 次のようになります.

// プログラムオブジェクトの作成
static GLuint createProgram(const char *vsrc, const char *pv, const char *fsrc, const char *fc)
{
  // バーテックスシェーダのシェーダオブジェクト
  GLuint vobj = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(vobj, 1, &vsrc, NULL);
  glCompileShader(vobj);
  
  // フラグメントシェーダのシェーダオブジェクトの作成
  GLuint fobj = glCreateShader(GL_FRAGMENT_SHADER);
  glShaderSource(fobj, 1, &fsrc, NULL);
  glCompileShader(fobj);
  
  // シェーダオブジェクトの取り付け
  GLuint program = glCreateProgram();
  glAttachShader(program, vobj);
  glDeleteShader(vobj);
  glAttachShader(program, fobj);
  glDeleteShader(fobj);
  
  // プログラムオブジェクトのリンク
  glBindAttribLocation(program, 0, pv);
  glBindFragDataLocation(program, 0, fc);
  glLinkProgram(program);
  
  return program;
}

シェーダプログラムの使用

図形の描画を行う前に, glUseProgram() を使って使用するプログラムオブジェクトを指定します.

  glUseProgram(program);
void glUseProgram(GLuint program)
program に指定したプログラムオブジェクトを描画に使用します. program に 0 を指定すると, どのプログラムオブジェクトも使用されなくなります.

ソースプログラムの変更点

main() 関数の前で前述のプログラムオブジェクトを作成する関数 createProgram() を定義し, OpenGL の初期設定 init() を実行した後で呼び出します. そして図形の描画を行う前に, この関数で作成したプログラムオブジェクトの使用を開始します.

...
 
int main(int argc, const char * argv[])
{
  // GLFW を初期化する
  if (!glfwInit())
  {
    // 初期化に失敗した
    std::cerr << "Can't initialize GLFW." << std::endl;
    exit(EXIT_FAILURE);
  }
  
  // OpenGL Version 3.2 Core Profile を選択する
  glfwOpenWindowHint(GLFW_OPENGL_VERSION_MAJOR, 3);
  glfwOpenWindowHint(GLFW_OPENGL_VERSION_MINOR, 2);
  glfwOpenWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
  
  // GLFW のウィンドウを開く
  if (!glfwOpenWindow(0, 0, 0, 0, 0, 0, 0, 0, GLFW_WINDOW))
  {
    // ウィンドウが開けなかった
    std::cerr << "Can't open GLFW window." << std::endl;
    exit(EXIT_FAILURE);
  }
  
  // 補助プログラムの初期化
  ggInit();
  
  // 開いたウィンドウに対する設定
  glfwSwapInterval(1);
  glfwSetWindowTitle("sample");
  
  // OpenGL の初期設定
  init();
  
  // バーテックスシェーダのソースプログラム
  static const GLchar vsrc[] =
    "#version 150 core\n"
    "in vec4 pv;\n"
    "void main(void)\n"
    "{\n"
    "  gl_Position = pv;\n"
    "}\n";
  
  // フラグメントシェーダのソースプログラム
  static const GLchar fsrc[] =
    "#version 150 core\n"
    "out vec4 fc;\n"
    "void main(void)\n"
    "{\n"
    "  fc = vec4(1.0, 0.0, 0.0, 0.0);\n"
    "}\n";
  
  // プログラムオブジェクトの作成
  GLuint program = createProgram(vsrc, "pv", fsrc, "fc");
  
  // 図形を表示する
  while (glfwGetWindowParam(GLFW_OPENED))
  {
    // 画面消去
    glClear(GL_COLOR_BUFFER_BIT);
    
    // シェーダプログラムの使用開始
    glUseProgram(program);
    
    /*
    ** ここで OpenGL による描画を行う
    */
    
    // シェーダプログラムの使用終了
    glUseProgram(0);
    
    glfwSwapBuffers();
  }
  
  return EXIT_SUCCESS;
}

glUseProgram(program); は, 使用するシェーダが一つしかなければ while ループの外 (前) に置いて構わないのですが, シェーダを図形 (あるいは描画単位) ごとに切り替えて使う場合は, ループの中に置くことになります. またシェーダは使いっぱなしでも構わないので, glUseProgram(0) は不要です.

図形の描画

OpenGL 3.2 の Core Profile では図形の描画に glBegin() ~ glEnd() が使えないだけでなく, 頂点配列も使えません. また頂点バッファオブジェクト (VBO) も直接描画に指定することはできません. 描画に指定できるのは, 頂点バッファオブジェクトを組み込んだ頂点配列オブジェクト (VAO) だけです.

頂点配列オブジェクトの作成

頂点配列オブジェクトは描画に使用するデータをまとめて管理します. これは glGenVertexArrays() を使って作成します. 作成した頂点配列オブジェクトを使用するときは, glBindVertexArray() を呼び出します.

  GLuint vao;
  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);
void glGenVertexArrays(GLsizei n, GLuint *arrays)
n 個の頂点配列オブジェクトを作成し, そのオブジェクト名 (整数値で表される識別子) を arrays に指定された配列の要素に格納します.
void glBindVertexArray(GLuint array)
arrays に指定されたオブジェクト名の頂点配列オブジェクトを結合します. 頂点配列オブジェクトは結合されている間使用できます. array が 0 の時は現在結合されている頂点配列オブジェクトの結合を解除します.

これに図形を登録します. 図形は以下の4点を頂点とする線図形にします. この4点を通る折れ線を, GL_LINE_LOOP で描きます. vertices は点の数です.

  static const GLfloat position[][2] =
  {
    { -0.5f, -0.5f },
    {  0.5f, -0.5f },
    {  0.5f,  0.5f },
    { -0.5f,  0.5f }
  };
  static const GLuint vertices = sizeof position / sizeof position[0];

頂点バッファオブジェクトを作成し, このデータをそこに送ります. 頂点バッファオブジェクトは GPU 側に確保したデータの保存領域を管理します. 転送するデータのサイズは, GLfloat 型の2次元の位置データが vertices 個なので,sizeof (GLfloat) * 2 * vertices になります.

  GLuint vbo;
  glGenBuffers(1, &vbo);
  glBindBuffer(GL_ARRAY_BUFFER, vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof (GLfloat) * 2 * vertices, position, GL_STATIC_DRAW);
void glGenBuffers(GLsizei n, GLuint *buffers)
n 個の頂点バッファオブジェクトを作成し, そのオブジェクト名 (整数値で表される識別子) を buffers に指定された配列の要素に格納します.
void glBindBuffer(GLenum target, GLuint buffer)
buffer に指定されたオブジェクト名の頂点バッファオブジェクトを target に結合します. 頂点バッファオブジェクトは結合されている間使用できます. buffer が 0 の時は現在 target に結合されている頂点バッファオブジェクトの結合を解除します. target には GL_ARRAY_BUFFER, GL_COPY_READ_BUFFER, GL_COPY_WRITE_BUFFER, GL_ELEMENT_ARRAY_BUFFER, GL_PIXEL_PACK_BUFFER, GL_PIXEL_UNPACK_BUFFER, GL_TEXTURE_BUFFER, GL_TRANSFORM_FEEDBACK_BUFFER, GL_UNIFORM_BUFFER が指定できます. 描画に使う頂点情報 (頂点属性) は, これらのうち GL_ARRAY_BUFFER に結合します.
void glBufferData(GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage)
target に現在結合されている頂点バッファオブジェクトのデータの保存領域 (メモリ) を size だけ確保し, そこに data で指定されたデータを転送します. usage はこの頂点バッファオブジェクトがどのような使われ方をするのかを指定します. GL_STREAM_DRAW, GL_STREAM_READ, GL_STREAM_COPY, GL_STATIC_DRAW, GL_STATIC_READ, GL_STATIC_COPY, GL_DYNAMIC_DRAW, GL_DYNAMIC_READ, GL_DYNAMIC_COPY が指定できます. これは GPU の動作を最適化するためのヒントとして利用されます.
STREAM
データの保存領域には一度だけ書き込まれ, 高々数回使用されます.
STATIC
データの保存領域には一度だけ書き込まれ, 何度も使用されます.
DYNAMIC
データの保存領域には繰り返し書き込まれ, 何度も使用されます.
DRAW
データの保存領域の内容はアプリケーションによって書き込まれ, 描画のためのデータとして用いられます.
READ
データの保存領域の内容はアプリケーションからの問い合わせによって OpenGL (GPU) 側から読み出され, アプリケーション側に返されます.
COPY
データの保存領域の内容はアプリケーションからの指令によって OpenGL (GPU) 側から読み出され, 描画のためのデータとして用いられます.

glVertexAttribPointer() を使って, この頂点バッファオブジェクト vbo をバーテックスシェーダの attribute 変数 pv で参照できるようにします. pv の位置は 0 ですので, 引数 index に 0 を指定します. これは2次元の位置データですから, size は 2 です. 引数 pointer には, 取り出すデータの先頭の場所を指定します. これが 0 なら, データは頂点バッファオブジェクトの先頭から取り出されます. その後 glEnableVertexAttribArray() で 位置が 0 の attribute 変数 pv の頂点配列からのデータの取得を有効にします.

  glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(0);
void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer);
レンダリング時に index に指定された位置にある attribute 変数が受け取るデータの場所と書式を指定します.
index
データを受け取る attribute 変数の位置を指定します.
size
attribute 変数が受け取る一個のデータのサイズを指定します. 1, 2, 3, 4 の値が指定できます. 一個のデータが二次元 (x, y) なら 2, 三次元 (x, y, z) なら 3 を指定します.
type
attribute 変数が受け取る (pointer で示された先の) データ型を指定します. GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, GL_UNSIGNED_INT, GL_FLOAT, GL_DOUBLE が指定できます.
normalized
GL_TRUE なら type が固定小数点型 (GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, GL_UNSIGNED_INT) のとき, その値をそのデータ型で表現可能な最大値で正規化します. GL_FALSE なら正規化しません.
stride
attribute 変数が受け取る (pointer で示された先の) データの配列の, 要素間の間隔を指定します. 0 ならデータは密に並んでいるものとして扱います.
pointer
attribute 変数が受け取るデータが格納されている場所を指定します. バイト単位のオフセットをポインタにキャストして渡します.
void glEnableVertexAttribArray(GLuint index)
レンダリング時に index に指定された位置にある attribute 変数のデータを配列から受け取るようにします.

最後に頂点バッファオブジェクトの結合を解除し, 頂点配列オブジェクトの結合を解除します. これらは結合しっぱなしでも何ら問題ないので, 省略しても動きます.

  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glBindVertexArray(0);

なお, 頂点バッファオブジェクトは別のところで作っておいて, glVertexAttribPointer() を呼び出す直前に glBindBuffer() で結合する, という書き方もできます.

頂点配列オブジェクトを作成する関数

以上の手続きを関数にまとめると, 次のようになります.

// 頂点配列オブジェクトの作成
static GLuint createObject(GLuint vertices, const GLfloat (*position)[2])
{
  // 頂点配列オブジェクト
  GLuint vao;
  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);
  
  // 頂点バッファオブジェクト
  GLuint vbo;
  glGenBuffers(1, &vbo);
  glBindBuffer(GL_ARRAY_BUFFER, vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof (GLfloat) * 2 * vertices, position, GL_STATIC_DRAW);
  
  // 結合されている頂点バッファオブジェクトを attribute 変数から参照できるようにする
  glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
  glEnableVertexAttribArray(0);
  
  // 頂点バッファオブジェクトと頂点配列オブジェクトの結合を解除する
  glBindBuffer(GL_ARRAY_BUFFER, 0);
  glBindVertexArray(0);
  
  return vao;
}

描画の実行

描画する図形データを保持した頂点配列オブジェクトを glBindVertexArray() で指定し, glDrawArrays() で描画します.

    glBindVertexArray(vao);
    glDrawArrays(GL_LINE_LOOP, 0, vertices);
    glBindVertexArray(0);
void glDrawArrays(GLenum mode, GLint first, GLsizei count)
頂点配列による図形の描画を行います. mode には GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_LINE_STRIP_ADJACENCY, GL_LINES_ADJACENCY, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, GL_TRIANGLES, GL_TRIANGLE_STRIP_ADJACENCY, GL_TRIANGLES_ADJACENCY が指定できます. first は描画する頂点配列の要素の先頭の番号, count には描画する頂点配列の要素の数を指定します.

この例では glBindVertexArray() をループの外に置くことができますが, 複数の図形を描画する際, 図形ごとに頂点配列オブジェクトを用意するなら, ループの中に置く必要があります. また, この場合も頂点配列オブジェクトを結合したままにして問題ないので, glBindVertexArray(0) は必要ありません.

ソースプログラムの変更点

main() 関数の前で前述の頂点配列オブジェクトを作成する関数 createObject() を定義し, 図形の表示を行う while ループの前で呼び出します. そしてループ中で図形の描画を行う前に, この関数で作成した頂点配列オブジェクトを結合します.

...
 
int main(int argc, const char * argv[])
{
  ...
  
  // OpenGL の初期設定
  init();
  
  // バーテックスシェーダのソースプログラム
  static const GLchar vsrc[] =
    "#version 150 core\n"
    "in vec4 pv;\n"
    "void main(void)\n"
    "{\n"
    "  gl_Position = pv;\n"
    "}\n";
  
  // フラグメントシェーダのソースプログラム
  static const GLchar fsrc[] =
    "#version 150 core\n"
    "out vec4 fc;\n"
    "void main(void)\n"
    "{\n"
    "  fc = vec4(1.0, 0.0, 0.0, 0.0);\n"
    "}\n";
  
  // プログラムオブジェクトの作成
  GLuint program = createProgram(vsrc, "pv", fsrc, "fc");
  
  // 図形データ
  static const GLfloat position[][2] =
  {
    { -0.5f, -0.5f },
    {  0.5f, -0.5f },
    {  0.5f,  0.5f },
    { -0.5f,  0.5f }
  };
  static const int vertices = sizeof position / sizeof position[0];
  
  // 頂点配列オブジェクトの作成
  GLuint vao = createObject(vertices, position);
  
  // 図形を表示する
  while (glfwGetWindowParam(GLFW_OPENED))
  {
    // 画面消去
    glClear(GL_COLOR_BUFFER_BIT);
    
    // シェーダプログラムの使用開始
    glUseProgram(program);
    
    // 図形の描画
    glBindVertexArray(vao);
    glDrawArrays(GL_LINE_LOOP, 0, vertices);
    glBindVertexArray(0);
    
    // シェーダプログラムの使用終了
    glUseProgram(0);
    
    glfwSwapBuffers();
  }
  
  return EXIT_SUCCESS;
}

エラーメッセージの表示

ここまではシェーダのコンパイルやリンクの時にエラーチェックを行っていませんでした. GLSL といえどもプログラミング言語なので, 書き間違えればエラーが発生します. その時にエラーメッセージがわからないと, どこが間違っているのか判断するのが難しくなります. そこで, コンパイルやリンクの際にエラーの発生をチェックし, エラーメッセージを表示する手続きを追加します.

// シェーダオブジェクトのコンパイル結果を表示する
static GLboolean printShaderInfoLog(GLuint shader, const char *str)
{
  // コンパイル結果を取得する
  GLint status;
  glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
  if (status == GL_FALSE) std::cerr << "Compile Error in " << str << std::endl;
  
  // シェーダのコンパイル時のログの長さを取得する
  GLsizei bufSize;
  glGetShaderiv(shader, GL_INFO_LOG_LENGTH , &bufSize);
  
  if (bufSize > 1)
  {
    // シェーダのコンパイル時のログの内容を取得する
    GLchar *infoLog = new GLchar[bufSize];
    GLsizei length;
    glGetShaderInfoLog(shader, bufSize, &length, infoLog);
    std::cerr << infoLog << std::endl;
    delete[] infoLog;
  }
  
  return (GLboolean)status;
}
// プログラムオブジェクトのリンク結果を表示する
static GLboolean printProgramInfoLog(GLuint program)
{
  // リンク結果を取得する
  GLint status;
  glGetProgramiv(program, GL_LINK_STATUS, &status);
  if (status == GL_FALSE) std::cerr << "Link Error" << std::endl;
  
  // シェーダのリンク時のログの長さを取得する
  GLsizei bufSize;
  glGetProgramiv(program, GL_INFO_LOG_LENGTH , &bufSize);
  
  if (bufSize > 1)
  {
    // シェーダのリンク時のログの内容を取得する
    GLchar *infoLog = new GLchar[bufSize];
    GLsizei length;
    glGetProgramInfoLog(program, bufSize, &length, infoLog);
    std::cerr << infoLog << std::endl;
    delete[] infoLog;
  }
  
  return (GLboolean)status;
}
void glGetShaderiv(GLuint shader, GLenum pname, GLint *params)
shader に指定したシェーダオブジェクトから pname に指定した情報を取り出し, params に格納します. *pname には GL_SHADER_TYPE, GL_DELETE_STATUS, GL_COMPILE_STATUS, GL_INFO_LOG_LENGTH,GL_SHADER_SOURCE_LENGTH が指定できます.
GL_SHADER_TYPE
shader に指定したシェーダオブジェクトのシェーダの種類 (GL_VERTEX_SHADER, GL_FRAGMENT_SHADER, GL_GEOMETRY_SHADER) を調べて *params に格納します.
GL_DELETE_STATUS
shader に指定したシェーダオブジェクトに glDeleteShader() によって削除マークが付けられてるかどうかを調べて, 削除マークがついていれば GL_TRUE, ついていなければ GL_FALSE を *params に格納します.
GL_COMPILE_STATUS
shader に指定したシェーダオブジェクトのコンパイルが成功したかどうかを調べて, 成功していれば GL_TRUE, 失敗していれば GL_FALSE を *params に格納します.
GL_INFO_LOG_LENGTH
shader に指定したシェーダオブジェクトのコンパイル時に生成されたログの長さを調べて *params に格納します. ログがなければ 0 を格納します.
GL_SHADER_SOURCE_LENGTH
shader に指定したシェーダオブジェクトのソースプログラムの長さを調べて *params に格納します. ソースプログラムがなければ 0 を格納します.
void glGetShaderInfoLog(GLuint shader, GLsizei maxLength, GLsizei *length, GLchar *infoLog)
shader に指定したシェーダオブジェクトのコンパイル時のログを, infoLog に指定された配列に取り出します. maxLength は取り出すログの最大の長さです. infoLog に指定した配列の大きさより小さくないとまずいです. *length には取り出されたログの長さが格納されます.
void glGetProgramiv(GLuint program, GLenum pname, GLint *params)
program に指定したプログラムオブジェクトから pname に指定した情報を取り出し, params に格納します. *pname には GL_DELETE_STATUS, GL_LINK_STATUS, GL_VALIDATE_STATUS, GL_INFO_LOG_LENGTH, GL_ATTACHED_SHADERS などが指定できます. これら以外についてはオンラインマニュアルの glGetProgram() の項目を参照してください.
GL_DELETE_STATUS
program に指定したプログラムオブジェクトに glDeleteProgram() によって削除マークが付けられてるかどうかを調べて, 削除マークがついていれば GL_TRUE, ついていなければ GL_FALSE を *params に格納します.
GL_LINK_STATUS
program に指定したプログラムオブジェクトのリンクが成功したかどうかを調べて, 成功していれば GL_TRUE, 失敗していれば GL_FALSE を *params に格納します.
GL_VALIDATE_STATUS
program に指定したプログラムオブジェクトに対して直前に行った glValidateProgram() による検証結果を調べて, *params に格納します. プログラムオブジェクトが現在の OpenGL の状態で実行可能なら GL_TRUE, 実行できなければ GL_FALSE を *params に格納します.
GL_INFO_LOG_LENGTH
program に指定したプログラムオブジェクトのリンク時に生成されたログの長さを調べて *params に格納します. ログがなければ 0 を格納します.
GL_ATTACHED_SHADERS
program に指定したプログラムオブジェクトに取り付けられているシェーダオブジェクトの数を調べて *params に格納します.
void glValidateProgram(GLuint program)
program に指定したプログラムオブジェクトが現在の OpenGL の状態で実行可能かどうかを調べます. 結果は glGetProgramiv() の引数 pname に GL_VALIDATE_STATUS を指定して取り出します.
void glGetProgramInfoLog(GLuint program, GLsizei maxLength, GLsizei *length, GLchar *infoLog)
program に指定したプログラムオブジェクトのリンク時のログを, infoLog に指定された配列に取り出します. maxLength は取り出すログの最大の長さです. infoLog に指定した配列の大きさより小さくないとまずいです. *length には取り出されたログの長さが格納されます.

これらの関数をプログラムオブジェクトを作成する関数 createProgram() を定義する前で定義し, シェーダオブジェクトのコンパイルの後やプログラムオブジェクトのコンパイルの後で呼び出します.

// プログラムオブジェクトの作成
static GLuint createProgram(const char *vsrc, const char *pv, const char *fsrc, const char *fc)
{
  ...
  
  // バーテックスシェーダのシェーダオブジェクト
  GLuint vobj = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(vobj, 1, &vsrc, NULL);
  glCompileShader(vobj);
  printShaderInfoLog(vobj, "vertex shader");
  
  ...
  
  // フラグメントシェーダのシェーダオブジェクトの作成
  GLuint fobj = glCreateShader(GL_FRAGMENT_SHADER);
  glShaderSource(fobj, 1, &fsrc, NULL);
  glCompileShader(fobj);
  printShaderInfoLog(fobj, "fragment shader");
  
  // シェーダオブジェクトの取り付け
  GLuint program = glCreateProgram();
  glAttachShader(program, vobj);
  glDeleteShader(vobj);
  glAttachShader(program, fobj);
  glDeleteShader(fobj);
  
  // プログラムオブジェクトのリンク
  glBindAttribLocation(program, 0, pv);
  glBindFragDataLocation(program, 0, fc);
  glLinkProgram(program);
  printProgramInfoLog(program);
  
  return program;
}

以上の設定を行ったソースファイルを, 下記に用意しています.

実行結果

実行結果

編集 «(3) OpenGL のバージョンとプロファイルの指定 最新 (5) パイプライン»