■ 2009年08月28日 [OpenGL][GLSL][ゼミ] 第4回 図形の描画
描画手順の変化
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_STRIP は GL_TRIANGLE_STRIP と, GL_POLYGONS は GL_TRIANGLE_FAN と全然変わらないので, これらが無くなっても, それほど支障はないと思います. 代わりに, ジオメトリシェーダを使う場合に使用できる GL_LINES_ADJACENCY, GL_LINE_STRIP_ADJACENCY, GL_TRIANGLES_ADJACENCY, および GL_TRIANGLE_STRIP_ADJACENCY が追加されました (このゼミでは, これらについては触れません).
頂点情報の頂点バッファオブジェクトへの転送
それでは, とりあえず始点と終点がつながった折れ線を描いてみましょう. これは基本図形に GL_LINE_LOOP を指定して, (0.9, 0.9), (-0.9, 0.9), (-0.9, -0.9), (0.9, -0.9) の4つの頂点を指定します.
描画には頂点バッファオブジェクトを使います. 最初に, 頂点バッファオブジェクトの名前を格納する変数 buffer を用意します.
... /* ** シェーダオブジェクト */ static GLuint vertShader; static GLuint fragShader; static GLuint gl2Program; /* ** 頂点バッファオブジェクト */ static GLuint buffer; ...
次に, 頂点バッファオブジェクトのメモリにアクセスするために使うポインタ変数を用意します.
... /* ** 初期化 */ static void init(void) { /* シェーダプログラムのコンパイル/リンク結果を得る変数 */ GLint compiled, linked; /* 頂点バッファオブジェクトのメモリを参照するポインタ */ typedef GLfloat Position[2]; Position *position; ...
頂点バッファオブジェクトを一つ作って, その名前を buffer に格納します.
... /* シェーダプログラムのリンク */ glLinkProgram(gl2Program); glGetProgramiv(gl2Program, GL_LINK_STATUS, &linked); printProgramInfoLog(gl2Program); if (linked == GL_FALSE) { fprintf(stderr, "Link error.\n"); exit(1); } /* 頂点バッファオブジェクトを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.9; position[0][1] = 0.9; position[1][0] = -0.9; position[1][1] = 0.9; position[2][0] = -0.9; position[2][1] = -0.9; position[3][0] = 0.9; position[3][1] = -0.9;
- void *glMapBuffer(GLenum target, GLenum access);
- バッファオブジェクトのメモリをアプリケーションプログラムのメモリ空間にマップします. 戻り値はマップされたメモリ空間の先頭のポインタです. target には glBindBuffer() と同じものを指定します. access には GL_READ_ONLY, GL_WRITE_ONLY, または GL_READ_WRITE が指定できます.
頂点情報の設定 (データの転送) が終わったら, 頂点バッファオブジェクトのメモリをプログラムのメモリ空間から切り離し, 頂点バッファオブジェクトを解放します.
/* 頂点バッファオブジェクトのメモリをプログラムのメモリ空間から切り離す */ glUnmapBuffer(GL_ARRAY_BUFFER); /* 頂点バッファオブジェクトを解放する */ glBindBuffer(GL_ARRAY_BUFFER, 0); } ...
- GLboolean glUnmapBuffer(GLenum target);
- バッファオブジェクトのメモリをプログラムのメモリ空間から切り離します. バッファオブジェクトのメモリの内容がマップされている間に破壊されていなければ, 戻り値は GL_TRUE になります. target には glBindBuffer() と同じものを指定します.
図形の描画
図形の描画は, 頂点バッファオブジェクトに格納した頂点情報を, バーテックスシェーダで宣言した attribute 変数 position に転送して行います. まず glUseProgram() を使って, 描画に使用するシェーダプログラムを適用します.
... /* ** 画面表示 */ static void display(void) { /* 画面クリア */ glClear(GL_COLOR_BUFFER_BIT); /* シェーダプログラムを適用する */ glUseProgram(gl2Program);
- void glUseProgram(GLuint program);
- シェーダプログラムを適用します. program にはシェーダプログラムの名前を指定します.
バーテックスシェーダの attribute 変数 position には, index として 0 を設定しましたから, これを指定して頂点バッファオブジェクトを有効にします. 頂点バッファオブジェクトには, 前に作成した buffer を指定します.
/* index が 0 の attribute 変数に頂点情報を対応付ける */ glEnableVertexAttribArray(0); /* 頂点バッファオブジェクトとして buffer を指定する */ glBindBuffer(GL_ARRAY_BUFFER, buffer);
- void glEnableVertexAttribArray(GLuint index);
- 頂点情報を attribute 変数に対応付ける. index には対応付ける attribute 変数に割り当てられた index を指定する.
頂点バッファオブジェクトのメモリには, 2x4 要素の GLfloat 型の配列が格納されています. これを attribute 変数 position に割り当てるので, glVertexAttribPointer() の第1引数には position の index である 0 を指定します. 第2引数は, position のデータ型が vec2 (2要素) なので, 2 を指定します. vec2 は GLfloat 型のベクトルなので, 第3引数には GL_FLOAT を指定します. 第4引数は, データ型が整数型であったときに, それを [0,1] または [-1,1] の範囲に正規化するか否かを指定します. ここでは正規化しないので, GL_FALSE を指定します. 第5引数には頂点情報と頂点情報の間隔を指定します. 頂点情報が密に (隙間無く) 格納されていれば, 0 を指定します. そして第6引数には, 頂点情報を格納している領域の先頭の位置を指定します. ここでは頂点情報は頂点バッファオブジェクトの先頭から格納されているので, 0 を指定します.
/* 頂点情報の格納場所と書式を指定する */ glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 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 で指定する.
そして図形を描画します. 描く図形は GL_LINE_LOOP です. 頂点バッファオブジェクトに格納されている頂点情報の, 0 番目から 4 個の頂点を描きます.
/* 図形を描く */ glDrawArrays(GL_LINE_LOOP, 0, 4);
- void glDrawArrays(GLenum mode, GLint first, GLsizei count);
- 頂点情報を転送して, 図形を描画する. mode には描画する基本図形の種類を指定する. first には描画するデータの, 格納場所の先頭からの位置を指定する. count には描画するデータの数を指定する.
図形の描画が完了したら, 頂点バッファオブジェクトを解放して, index が 0 の attribute 変数に対応する頂点バッファオブジェクトを無効にします.
/* 頂点バッファオブジェクトを解放する */ glBindBuffer(GL_ARRAY_BUFFER, 0); /* index が 0 の attribute 変数の頂点情報との対応付けを解除する */ glDisableVertexAttribArray(0); glFlush(); } ...
- void glDisableVertexAttribArray(GLuint index);
- attribute 変数と頂点情報の対応付けを解除する. index には対応付けを行った attribute 変数に割り当てられた index を指定する.
これで下のような図形が描かれれば OK です.
一応, ここまでのプログラムをまとめたものを, 以下に用意しておきます.
床井先生、<br>いつもお世話になっております。<br>複数のオブジェクトを読み込んで、レンダリングしたいのですが、その場合、VAOやVBOは複数作らなければならないのでしょうか?
横槍、失礼いたします^^;<br>異なる複数のオブジェクトの場合、大抵はVAOやVBOを分けることが多いですが、やろうと思えば全部一つのVAO、VBOに詰め込んでしまうこともできます(後者の場合は、それぞれのオブジェクトごとに、VBOのどの箇所をどのようにアクセスするのか、を設定します)。<br>ただ、別々に作成した方がやはりわかりやすいと思います。
emadurandal さま,コメントありがとうございます.<br>たーさま,emadurandal さまに詳しく解説いただいていいます.<br>https://qiita.com/emadurandal/items/0bb83b545670475f51a3<br>コメントにリンクがうまく張れなくなっているみたいなので,手作業でリンクを埋めました.<br><br>ちなみに,いま書いている資料(本)では,そのようにしています.まだ公開していませんw
あと UBO も使うようにしています.
質問なのですが、genBufferで生成するbufferの型がGLuintである意味はあるのでしょうか?
しろ様、コメントありがとうございます。<br>buffer が 0 のときはどのバッファオブジェクトも割り当てられていないことを表し、有効なバッファオブジェクトでは buffer > 0 ですから、GLuint を用いるのが適切かと思います。<br>また glGenBffers() の関数定義は void glGenBuffers(GLsizei n, GLuint *buffers); となっており、GLuint は typedef unsigned int GLuint; と定義されていますので、buffer が GLuint か unsigned int なら問題ありませんが、それ以外の型だと C++ ではコンパイルエラーになり、C だと(コンパイルオプションによっては)警告が出ます。これはプログラムの実行時エラーや、仮に実行できてもタチの悪いバグの原因になりますので、避けるべきかだと考えます。
お返事ありがとうございます。では glGenBuffersで確保したいメモリの先頭アドレスを決め glBufferDataで全体のメモリ確保,データの転送という感じでしょうか
しろ様,お返事が遅くなり申し訳ありません.<br>glGenBuffer ではメモリの確保等は行いません.単にバッファオブジェクト名 (buffer object names, 管理用のハンドルみたいなものだと思います) の生成を行います.glBufferData の呼び出しにより,実際にメモリが確保されます.