床井研究室

補助プログラム

前回 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() を実行してください。

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

#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
#include <GL/glew.h>
#include <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)
{
  gl_Position = pv;
}

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

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

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

  GLuint shader = glCreateShader(GL_VERTEX_SHADER);
  glShaderSource(shader, sizeof source / sizeof source[0], source, 0);
  glCompileShader(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"
    "  gl_Position = 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);
  ...

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

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

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

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

  glBindAttribLocation(program, 0, "pv");
  glBindFragDataLocation(program, 0, "fc");
  glLinkProgram(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);

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

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) は不要です1

図形の描画

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

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

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

  GLuint vao;
  glGenVertexArrays(1, &vao);
  glBindVertexArray(vao);

これに図形を登録します。図形は以下の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);

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);

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

  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);

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

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

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;
}

これらの関数をプログラムオブジェクトを作成する関数 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;
}

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

実行結果

実行結果

  1. 後でトラブルになるから、そういう手抜きはするなって言われました。(2026 年 8 月 31 日 追記) 

  2. これも後でトラブルになるから、手抜きはするなって言われました。(2026 年 8 月 31 日 追記) 

  3. 同上。