床井研究室

描画手順の変化

OpenGL では、描画する基本図形 (primitive) の種類を指定した後、図形を構成する頂点情報を送って図形を描画します。従来は glBegin() で描画する基本図形を指定し、glEnd() までの間で glVertex*()glNormal*(), glTexCoord*() などで頂点情報を送ることができました。OpenGL 3.0 以降において前方互換を指定すると、これらは使えなくなります。

代わりに、図形の描画には頂点配列 (vertex array) や頂点配列オブジェクト (vertex array object, VAO), あるいは頂点バッファオブジェクト (vertex buffer object, VBO) を使用します。

頂点配列
頂点配列は頂点情報を配列に格納しておき、描画時に CPU から GPU に向けて一気に転送します。
頂点配列オブジェクト
頂点配列オブジェクトは頂点情報をクライアントメモリ (アプリケーション側に割り当てたメモリ) に格納しておき、これに「名前」を付けて管理できるようにしたものです。
頂点バッファオブジェクト
頂点バッファオブジェクトは頂点配列オブジェクトに似ていますが、OpenGL 側 (グラフィックスハードウェア) にメモリを確保し、そこにあらかじめ頂点情報を転送しておく点が異なります。この方法は描画時に CPU から GPU へのデータ転送を行わないので、GPU の性能を最大限に引き出すことができますが、グラフィックスハードウェアに十分なメモリが必要です。

また、頂点情報は一般に位置や法線ベクトル、頂点色、テクスチャ座標などから構成されますが、これらは区別されなくなり、バーテックスシェーダで参照される attribute 変数 (in 変数) に一本化されました。開発者は必要に応じて attribute 変数を定義し、それに頂点情報を設定して、GPU のバーテックスシェーダに送ります。

基本図形の変化

OpenGL 3.0 以降では、基本図形にも変更が加えられています。GL_QUADS, GL_QUAD_STRIP, および GL_POLYGONS は使えなくなりました。GL_QUADS は、実際には直方体を描くときくらいしか使い道がない?気がしますし、GL_QUAD_STRIPGL_TRIANGLE_STRIP と、GL_POLYGONSGL_TRIANGLE_FAN と全然変わらないので、これらが無くなっても、それほど支障はないと思います。代わりに、ジオメトリシェーダを使う場合に使用できる GL_LINES_ADJACENCY, GL_LINE_STRIP_ADJACENCY, GL_TRIANGLES_ADJACENCY, および GL_TRIANGLE_STRIP_ADJACENCY が追加されました (このゼミでは、これらについては触れません)。

OpenGL の基本図形

頂点情報の頂点バッファオブジェクトへの転送

それでは、とりあえず始点と終点がつながった折れ線を描いてみましょう。これは基本図形に GL_LINE_LOOP を指定して、(0.9, 0.9), (-0.9, 0.9), (-0.9, -0.9), (0.9, -0.9) の4つの頂点を指定します。

折れ線図形

描画には頂点バッファオブジェクトを使います。最初に、頂点バッファオブジェクトの名前を格納する変数 buffer を用意します。

...

/*
** プログラムオブジェクト
*/
static GLuint gl2Program;

/*
** attribute 変数 position の頂点バッファオブジェクト
*/
static GLuint buffer;

...

次に、頂点バッファオブジェクトのメモリにアクセスするために使うポインタ変数を用意します。

...

/*
** 初期化
*/
static void init(void)
{
  /* シェーダオブジェクトの作成 */
  GLuint vertShader = glCreateShader(GL_VERTEX_SHADER);
  GLuint fragShader = glCreateShader(GL_FRAGMENT_SHADER);

  /* シェーダのソースプログラムの読み込み */
  if (readShaderSource(vertShader, "simple.vert")) exit(1);
  if (readShaderSource(fragShader, "simple.frag")) exit(1);

  /* シェーダオブジェクトのコンパイル結果を得る変数 */
  GLint compiled;

...

バーテックスシェーダの attribute 変数 positionindex として 0 を割り当ててから、シェーダプログラムをリンクします。

...

  /* attribute 変数 position の index を 0 に指定する */
  glBindAttribLocation(gl2Program, 0, "position");

  /* シェーダプログラムのリンク */
  glLinkProgram(gl2Program);
  glGetProgramiv(gl2Program, GL_LINK_STATUS, &linked);
  printProgramInfoLog(gl2Program);
  if (linked == GL_FALSE) {
    fprintf(stderr, "Link error.\n");
    exit(1);
  }

シェーダプログラムのリンクが完了したら、index が 0 の attribute 変数を有効にします。

  /* index が 0 の attribute 変数を有効にする */
  glEnableVertexAttribArray(0);
void glEnableVertexAttribArray(GLuint index);
頂点情報を attribute 変数に対応付ける機能を有効にします。index には有効にする attribute 変数に割り当てられた index を指定します。

頂点バッファオブジェクトを一つ作って、その名前を buffer に格納します。

  /* 頂点バッファオブジェクトを1つ作る */
  glGenBuffers(1, &buffer);
void glGenBuffers(GLsizei n, GLuint* buffers);
バッファオブジェクトを作成します。n に作成するバッファオブジェクトの数を指定します。作成されたバッファオブジェクトの名前 (番号) が buffers に指定した配列に n 個入ります。

作成した頂点バッファオブジェクトのメモリ領域を確保します。頂点情報は2次元の頂点位置が4個ですから、確保するメモリのサイズは sizeof (Position) * 4 バイトです。また、この時点では頂点バッファオブジェクトのメモリへのデータの転送を行いませんから、glBufferData() の第3引数は NULL にしておきます。このメモリは、最初にデータを書き込んだ後、変更せずに繰り返して描画に使うので、この第4引数は GL_STATIC_DRAW にします。

  /* 頂点バッファオブジェクトに4頂点分のメモリ領域を確保する */
  glBindBuffer(GL_ARRAY_BUFFER, buffer);
  glBufferData(GL_ARRAY_BUFFER, sizeof (Position) * 4, NULL, GL_STATIC_DRAW);
void glBindBuffer(GLenum target, GLuint buffer);
バッファオブジェクトを有効にします。頂点バッファオブジェクトの場合、target には GL_ARRAY_BUFFER を指定します。glDrawElements() で指定する頂点のインデックスの場合は GL_ELEMENT_ARRAY_BUFFER を指定します。buffer には有効にするバッファオブジェクトの名前を指定します。0 を指定した場合は、現在有効になっているバッファオブジェクトを無効にします。
void glBufferData(GLenum target, GLsizeiptr size, const GLvoid *data, GLenum usage);
バッファオブジェクトのメモリを確保し、そこにデータを転送します。target には glBindBuffer() と同じものを指定します。size には確保するメモリのサイズを byte で指定します。data は転送元のデータの配列を指定します。ここに NULL を指定するとメモリの確保だけが行われ、データの転送は行ないません。usage にはバッファオブジェクトのメモリの使われ方のヒントを指定します。これは GL_XXXX_YYYY という形式の記号定数であり、XXXX と YYYY には、それぞれ以下のものが指定できます。
  • XXXX = STREAM: メモリには一度しか書き込まれず、使われる回数も少ない。
  • XXXX = STATIC: メモリには一度しか書き込まれないが、何回も使われる。
  • XXXX = DYNAMIC: メモリには何回も書き込まれ、何回も使われる。
  • YYYY = DRAW: メモリの内容はアプリケーションプログラムから書き込まれ、OpenGL による描画データとして使用される。
  • YYYY = READ: メモリの内容は OpenGL によって書き込まれ、アプリケーションプログラムによって読み出される。
  • YYYY = COPY: メモリの内容は OpenGL によって書き込まれ、OpenGL による描画データとして使用される。

確保した頂点バッファオブジェクトのメモリ領域をプログラムのメモリ空間にマップ (重ね合わせ) し、その先頭のポインタをポインタ変数 position に得ます。こうすることにより、頂点バッファオブジェクトのメモリをプログラムが確保したメモリと同様に扱うことができます。この変数 position を介して、データ (頂点位置) を設定します。

glBufferData()glBufferSubData() では、テクスチャオブジェクトのメモリ領域に転送するデータを、あらかじめ配列に格納しておく必要があります。配列を用意したくない時や、連続していないデータの一部を頻繁に変更するような場合は、glMapBuffer() を使うと便利です。

  /* 頂点バッファオブジェクトのメモリをプログラムのメモリ空間にマップする */
  position = (Position *)glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);

  /* 頂点バッファオブジェクトのメモリにデータを書き込む */
  position[0][0] =  0.9f;
  position[0][1] =  0.9f;
  position[1][0] = -0.9f;
  position[1][1] =  0.9f;
  position[2][0] = -0.9f;
  position[2][1] = -0.9f;
  position[3][0] =  0.9f;
  position[3][1] = -0.9f;
void *glMapBuffer(GLenum target, GLenum access);
バッファオブジェクトのメモリをアプリケーションプログラムのメモリ空間にマップします。戻り値はマップされたメモリ空間の先頭のポインタです。target には glBindBuffer() と同じものを指定します。access には GL_READ_ONLY, GL_WRITE_ONLY, または GL_READ_WRITE が指定できます。

頂点情報の設定 (データの転送) が終わったら、頂点バッファオブジェクトのメモリをプログラムのメモリ空間から切り離します。

  /* 頂点バッファオブジェクトのメモリをプログラムのメモリ空間から切り離す */
  glUnmapBuffer(GL_ARRAY_BUFFER);
GLboolean glUnmapBuffer(GLenum target);
バッファオブジェクトのメモリをプログラムのメモリ空間から切り離します。バッファオブジェクトのメモリの内容がマップされている間に破壊されていなければ、戻り値は GL_TRUE になります。target には glBindBuffer() と同じものを指定します。

そして、頂点バッファオブジェクトに格納した頂点情報を、バーテックスシェーダの attribute 変数 position (index が 0) に対応付けます。

頂点バッファオブジェクトのメモリには、$2 \times 4$ 要素の GLfloat 型の配列が格納されています。これを attribute 変数 position に割り当てるので、glVertexAttribPointer() の第1引数には positionindex である 0 を指定します。第2引数は、position のデータ型が vec2 (2要素) なので、2 を指定します。vec2 は GLfloat 型のベクトルなので、第3引数には GL_FLOAT を指定します。第4引数は、データ型が整数型であったときに、それを $[0,1]$ または $[-1,1]$ の範囲に正規化するか否かを指定します。ここでは正規化しないので、GL_FALSE を指定します。第5引数には頂点情報と頂点情報の間隔を指定します。頂点情報が密に (隙間無く) 格納されていれば、0 を指定します。そして第6引数には、頂点情報を格納している領域の先頭の位置を指定します。ここでは頂点情報は頂点バッファオブジェクトの先頭から格納されているので、0 を指定します。設定が終わったら、頂点バッファオブジェクトを解放します。

  /* 頂点バッファオブジェクトを解放する */
  glBindBuffer(GL_ARRAY_BUFFER, 0);
}

...
void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer);
頂点情報の格納場所と書式を指定します。index には attribute 変数に割り当てられた index を指定します。size はデータの1頂点あたりの要素の数で、1〜4 を指定します(2次元座標なら 2)。type には格納されているデータの形式を指定します。GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT, GL_UNSIGNED_SHORT, GL_INT, GL_UNSIGNED_INT, GL_FLOAT, GL_DOUBLE が指定できます。stride には格納されているデータの間隔を byte で指定します。0 を指定したときは、データは密に並んでいるとみなされます。pointer にデータが格納されている場所を指定します。頂点配列の場合は、配列のポインタを指定します。頂点バッファオブジェクトの場合はバッファオブジェクトのメモリの先頭からの位置を byte で指定します。

図形の描画

図形の描画は、まず glUseProgram() を使って、描画に使用するシェーダプログラムを適用します。

/*
** 画面表示
*/
static void display(void)
{
  /* 画面クリア */
  glClear(GL_COLOR_BUFFER_BIT);

  /* プログラムオブジェクトを適用する */
  glUseProgram(gl2Program);
void glUseProgram(GLuint program);
シェーダプログラムを適用します。program にはシェーダプログラムの名前を指定します。

そして、頂点バッファオブジェクト buffer を指定して図形を描画します。描く図形は GL_LINE_LOOP です。頂点バッファオブジェクトに格納されている頂点情報の、0 番目から 4 個の頂点を描きます。図形の描画が完了したら、頂点バッファオブジェクトを解放します。

  /* 頂点バッファオブジェクトとして buffer を指定する */
  glBindBuffer(GL_ARRAY_BUFFER, buffer);

  /* index が 0 の attribute 変数を有効にする */
  glEnableVertexAttribArray(0);

  /* index が 0 の attribute 変数に頂点バッファオブジェクトの場所と書式を設定する */
  glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);

  /* 図形を描く */
  glDrawArrays(GL_LINE_LOOP, 0, 4);

  /* index が 0 の attribute 変数を無効にする */
  glDisableVertexAttribArray(0);

  /* 頂点バッファオブジェクトを解放する */
  glBindBuffer(GL_ARRAY_BUFFER, 0);

  /* 固定機能に戻す*/
  glUseProgram(0);

  glFlush();
}
void glDrawArrays(GLenum mode, GLint first, GLsizei count);
頂点情報を転送して、図形を描画します。mode には描画する基本図形の種類を指定します。first には描画するデータの、格納場所の先頭からの位置を指定します。count には描画するデータの数を指定します。

これで下のような図形が描かれれば OK です。

クリッピング空間への描画