OpenGLプログラミング/モダンOpenGL チュートリアル 03
Attributes:追加的な頂点情報を渡す
[編集]素の座標を超えるものがプログラムで必要になるかもしれません。 例えば:色。 OpenGLにRGBカラー情報を渡してみましょう。
私たちは座標を渡すときに attributeを使用してきましたが、そのように色のための新しい属性を追加することができます。 私たちのグローバルを変更してみましょう:
GLuint vbo_triangle, vbo_triangle_colors;
GLint attribute_coord2d, attribute_v_color;
そして init_resources:
GLfloat triangle_colors[] = {
1.0, 1.0, 0.0,
0.0, 0.0, 1.0,
1.0, 0.0, 0.0,
};
glGenBuffers(1, &vbo_triangle_colors);
glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle_colors);
glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_colors), triangle_colors, GL_STATIC_DRAW);
[...]
attribute_name = "v_color";
attribute_v_color = glGetAttribLocation(program, attribute_name);
if (attribute_v_color == -1) {
fprintf(stderr, "Could not bind attribute %s\n", attribute_name);
return 0;
}
今 onDisplay
処理手続の中で、3つの頂点のそれぞれについて、1 RGBカラーを渡すことができます。 私は黄色、青、赤を選びましたが、自由にあなたの好きな色を使用してかまいません :)
glEnableVertexAttribArray(attribute_v_color);
glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle_colors);
glVertexAttribPointer(
attribute_v_color, // attribute
3, // number of elements per vertex, here (r,g,b)
GL_FLOAT, // the type of each element
GL_FALSE, // take our values as-is
0, // no extra data between each position
0 // offset of first element
);
関数の最後で、属性に関しては完了したと、OpenGLに一度伝えます:
glDisableVertexAttribArray(attribute_v_color);
最後に、頂点シェーダでも、それを宣言します:
attribute vec3 v_color;
この時点で、プログラムを実行すると、次のようになります:
Could not bind attribute v_color
まだv_colorを使用していなかったためです。 [1]
問題は:フラグメントシェーダで色をつけたいのであって、頂点シェーダではないのです! 今からやり方を見ていきましょう ...
Varyings: 頂点シェーダからの情報をフラグメントシェーダに渡す
[編集]属性を使用する代わりに、 varying 変数を使用します。 それは次のようなものです:
- 頂点シェーダの output 変数
- フラグメントシェーダの input 変数
- It's interpolated.
つまり2つのシェーダ間の通信チャネルです。 それが補間される理由を理解するために、例を見てみましょう。
新しいvarying、たとえば f_color
を、両方のシェーダで宣言する必要があります。
テンプレート:Yellow Warning
triangle.v.glsl で:
attribute vec2 coord2d;
attribute vec3 v_color;
varying vec3 f_color;
void main(void) {
gl_Position = vec4(coord2d, 0.0, 1.0);
f_color = v_color;
}
そして triangle.f.glsl で:
varying vec3 f_color;
void main(void) {
gl_FragColor = vec4(f_color.x, f_color.y, f_color.z, 1.0);
}
(注:GLES2を使用している場合は、下記の移植性のセクションを確認してください。)
結果を見てみましょう:
ワオ、実際に3色以上になっています!
OpenGLが各画素の頂点の値を補間しています。 これが{0}varying{/0}という名前の理由です: 各頂点ごとに変化し、そしてさらに各フラグメントごとに変化します。
Cコードではvaryingを宣言する必要はありませんでした - Cコードとvaryingの間のインターフェイスがないからです。
混交座標と色
[編集]glVertexAttribPointer関数をより良く理解するために、ひとつのC配列のなかに両方の属性を混在させてみましょう:
GLfloat triangle_attributes[] = {
0.0, 0.8, 1.0, 1.0, 0.0,
-0.8, -0.8, 0.0, 0.0, 1.0,
0.8, -0.8, 1.0, 0.0, 0.0,
};
glGenBuffers(1, &vbo_triangle);
glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);
glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_attributes), triangle_attributes, GL_STATIC_DRAW);
glVertexAttribPointer
の第5要素は strideで、属性の各セットがどれくらい長さなのかをOpenGLに伝えます - この場合には浮動小数点数で5です:
glEnableVertexAttribArray(attribute_coord2d);
glEnableVertexAttribArray(attribute_v_color);
glBindBuffer(GL_ARRAY_BUFFER, vbo_triangle);
glVertexAttribPointer(
attribute_coord2d, // attribute
2, // number of elements per vertex, here (x,y)
GL_FLOAT, // the type of each element
GL_FALSE, // take our values as-is
5 * sizeof(GLfloat), // next coord2d appears every 5 floats
0 // offset of the first element
);
glVertexAttribPointer(
attribute_v_color, // attribute
3, // number of elements per vertex, here (r,g,b)
GL_FLOAT, // the type of each element
GL_FALSE, // take our values as-is
5 * sizeof(GLfloat), // next color appears every 5 floats
(GLvoid*) (2 * sizeof(GLfloat)) // offset of first element
);
ちょうど同じように動作します!
注意点として、色に関してのものは配列の3つめの要素で始まり (2 * sizeof(GLfloat)
)、最初の色の場所がその最初の要素の{1}オフセット{/1}になります。
なぜ (GLvoid*)
なのでしょう ? お見せしたように、OpenGLの初期のバージョンでは、ポインタをCの配列に直接渡すことが可能でした(バッファオブジェクトではなく)。 これはもう廃止されていますが、 glVertexAttribPointer
プロトタイプは変わらず残っているので、まるでポインタを渡すようなことをしますが、本当はオフセット渡しています。
娯しみのための代替案:
struct attributes {
GLfloat coord2d[2];
GLfloat v_color[3];
};
struct attributes triangle_attributes[] = {
{{ 0.0, 0.8}, {1.0, 1.0, 0.0}},
{{-0.8, -0.8}, {0.0, 0.0, 1.0}},
{{ 0.8, -0.8}, {1.0, 0.0, 0.0}},
};
...
glBufferData(GL_ARRAY_BUFFER, sizeof(triangle_attributes), triangle_attributes, GL_STATIC_DRAW);
...
glVertexAttribPointer(
...,
sizeof(struct attributes), // stride
(GLvoid*) offsetof(struct attributes, v_color) // offset
...
最初のカラーオフセットを指定するための offsetof
の使い方に注意しましょう。
Uniforms: グローバル情報を渡す
[編集]attribute変数の反対は、 uniform 変数です:すべての頂点で同じになります。 Cコードからそれらを定期的に変更できることに気をつけましょう - しかし、頂点のセットを画面上に表示されるときはいつでも、uniformは一定です。
Cコードから三角形のグローバル透明度を定義したいとしましょう。 属性と同様に、それを宣言する必要があります。 Cコードのグローバル:
GLint uniform_fade;
次にCコードでそれを宣言します(プログラムをリンクした後の状態で):
const char* uniform_name;
uniform_name = "fade";
uniform_fade = glGetUniformLocation(program, uniform_name);
if (uniform_fade == -1) {
fprintf(stderr, "Could not bind uniform %s\n", uniform_name);
return 0;
}
注:シェーダコード内の特定の配列要素を、 uniform_name
、例えば "my_array[1]"
などでターゲットすることもできます !
加えて、uniformにおいて、varyingでない値を明示的に設定します。 さあ、 onDisplay
でリクエストして、三角形の不透明度をごくわずかにしましょう:
glUniform1f(uniform_fade, 0.1);
今、この変数をフラグメントシェーダで使用することができます:
varying vec3 f_color;
uniform float fade;
void main(void) {
gl_FragColor = vec4(f_color.x, f_color.y, f_color.z, fade);
}
注:コード内でuniformを使用しない場合、 glGetUniformLocation
はそれを認識せず、失敗します。
OpenGL ES 2のポータビリティ
[編集]前のセクションでは、GLES2が精度のヒントを必要とすることを述べました。 これらのヒントは、データにどのくらいの精度を求めるのかをOpenGLに伝えます。 指定できる精度は:
lowp
mediump
highp
例えば、 lowp
は色に使用されることが多く、そして頂点には highp
の使用を推奨します。
各変数の精度を指定することができます:
varying lowp vec3 f_color;
uniform lowp float fade;
または、デフォルトの精度を宣言することもできます:
precision lowp float;
varying vec3 f_color;
uniform float fade;
残念ながら、これらの精度のヒントは、従来のOpenGL 2.1では動作しないので、それらをインクルードするのはGLES2の場合のみにする必要があります。
GLSLにはCプリプロセッサに似たプリプロセッサが含まれています。 #define
や #ifdef
のようなディレクティブを使用することができます。
フラグメントシェーダだけは、浮動小数点において明示的な精度を必要とします。 精度は頂点シェーダでは暗黙的に highp
になります。 フラグメントシェーダでは、 highp
を使用できない場合があり、これは GL_FRAGMENT_PRECISION_HIGH
マクロを使用してテストできます [2]。
私たちはすでに、 create_shader
ユーティリティ関数を通じてマクロ処理ディレクティブを先頭に追加しています:
#define GLES2
シェーダーローダーを改善してGLES2のデフォルトの精度を定義することもでき、そしてOpenGL 2.1では精度の識別子を無視することもできます(なので、必要に応じて特定の変数の精度を設定することもできます):
GLuint res = glCreateShader(type);
const GLchar* sources[] = {
// Define GLSL version
#ifdef GL_ES_VERSION_2_0
"#version 100\n"
#else
"#version 120\n"
#endif
,
// GLES2 precision specifiers
#ifdef GL_ES_VERSION_2_0
// Define default float precision for fragment shaders:
(type == GL_FRAGMENT_SHADER) ?
"#ifdef GL_FRAGMENT_PRECISION_HIGH\n"
"precision highp float; \n"
"#else \n"
"precision mediump float; \n"
"#endif \n"
: ""
// Note: OpenGL ES automatically defines this:
// #define GL_ES
#else
// Ignore GLES 2 precision specifiers:
"#define lowp \n"
"#define mediump\n"
"#define highp \n"
#endif
,
source };
glShaderSource(res, 3, sources, NULL);
エラーメッセージを表示する際に、GLSLコンパイラはその行数のうちのこれらの前に付加された行をカウントすることに留意してください。 残念なことに、#line 0
に設定しても、このコンパイラ行カウントはリセットされません。
ディスプレイをリフレッシュする
[編集]さて、透明度を行ったり来たり変化させることができれば、かなり素敵です。 これを達成するために、
- ユーザーがアプリケーションを起動してからの秒数をチェックすることができます。
glutGet(GLUT_ELAPSED_TIME)/1000
がおしえてくれます - その上に数学の sin 関数を適用します ( sin関数は単位時間2.PI=〜6.28ごとに-1と+1の間で行ったり来たり動きます )
- 三角形を(新しいフェード値で)再表示するようアプリケーションに要求します。これは
glutPostRedisplay()
- GLUT には "idle"関数があり、これはGLUTがそれ以上何のすることがない時に呼び出されるもので(たとえば三角形を表示し終わったとき)、私たちのコードをそこに置くことができます
main内で、 "idle"関数を glutDisplayFunc(...)
の後に宣言しましょう:
glutIdleFunc(idle);
新しい idle関数を追加しましょう
void idle()
{
float cur_fade = sinf(glutGet(GLUT_ELAPSED_TIME) / 1000.0 * (2*M_PI) / 5) / 2 + 0.5; // 0->1->0 every 5 seconds
glUseProgram(program);
glUniform1f(uniform_fade, cur_fade);
glutPostRedisplay();
}
また、 onDisplay
でglUniform1f
の呼び出しも削除しましょう。
最後に、Cのmathライブラリの使用を宣言する必要があります:
#include <math.h>
そしてMakefileの中で:
LDLIBS=-lglut -lGLEW -lGL -lm
コンパイルして実行しましょう ...
私たちは最初のアニメーションにたどりつきました !
OpenGLの実装は一般的に、物理画面のバッファを更新する前に、画面の垂直リフレッシュを待ちます - これは垂直同期と呼ばれています。 その場合、三角形は毎秒60回(60 FPS)レンダリングされます。 垂直同期を無効にした場合、三角形を連続的に更新するようになり、その結果プログラムのCPU使用率が高くなってしまいます。 垂直同期には、視点の変更を使用してアプリケーションを作成するときに再び出会います。
Notes
[編集]- ^ It's a bit confusing to have a single example across the two differents concepts of attribute and varying. We'll try to find two separate examples to better explain them.
- ^ Cf. “OpenGL ES Shading Language 1.0.17 Specification”. Khronos.org (2009年5月12日). 2011年9月10日閲覧。, section 4.5.3 Default Precision Qualifiers