OpenGLプログラミング/モダンOpenGL チュートリアル 05
三角形のアニメーションも楽しいですが、私たちがOpenGLを学んでいるのは3Dグラフィックスを表示するためです。
キューブを作成してみましょう!
3つめの次元の追加
[編集]キューブは、3D空間では8頂点です(前面に4点、背面に4点)。
triangle
を cube
にリネームしてもよいでしょう。
また fade
バインディングもコメントアウトしましょう。
さてキューブの頂点を書いてみましょう。 この写真のように(X、Y、Z)座標系を配置します。 これらをオブジェクトの中心に関連づけて書いていきます。 クリーンになり、後でその中心の周りに立方体を回転させることができるようになります:
注:ここで、Z座標はユーザーの手前向きです。 BlenderではZが上(高さ)のような他の規約を見ることもあるかもしれませんが、OpenGLのデフォルトはYが上です
GLfloat cube_vertices[] = {
// front
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
// back
-1.0, -1.0, -1.0,
1.0, -1.0, -1.0,
1.0, 1.0, -1.0,
-1.0, 1.0, -1.0,
};
黒いカタマリよりは良いものを表示するために、色も定義しましょう:
GLfloat cube_colors[] = {
// front colors
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
1.0, 1.0, 1.0,
// back colors
1.0, 0.0, 0.0,
0.0, 1.0, 0.0,
0.0, 0.0, 1.0,
1.0, 1.0, 1.0,
};
グローバルバッファのハンドルも忘れずに: :
GLuint vbo_cube_vertices, vbo_cube_colors;
Elements - Index Buffer Objects (IBO)
[編集]私たちのキューブは6つの面を持っています。 2つの面が頂点を共有することもあります。 そのうえ、面を2つの三角形の組み合わせとして記述します(合計で12の三角形)。
そこで、elementというコンセプトを紹介します: glDrawElements
を、 glDrawArrays
の代わりに使用します。 これは、頂点配列を参照するインデックスの集合をとります。 glDrawElements
を使って、順序を任意に指定でき、さらに同じ頂点を複数回指定することもできます。 私たちは、これらのインデックスをIndex Buffer Object(IBO)に格納します。
すべての面を同様の方法、ここでは反時計回りで指定したほうがよいのは、テクスチャマッピング(次のチュートリアル参照)と照明(三角形の法線が正しい道を指し示す必要があるので)で重要となるからです。
/* Global */
GLuint ibo_cube_elements;
/* init_resources */
GLushort cube_elements[] = {
// front
0, 1, 2,
2, 3, 0,
// top
3, 2, 6,
6, 7, 3,
// back
7, 6, 5,
5, 4, 7,
// bottom
4, 5, 1,
1, 0, 4,
// left
4, 0, 3,
3, 7, 4,
// right
1, 5, 6,
6, 2, 1,
};
glGenBuffers(1, &ibo_cube_elements);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_cube_elements);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(cube_elements), cube_elements, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
再びバッファオブジェクトを使用していますが、 GL_ARRAY_BUFFER
の代わりに GL_ELEMENT_ARRAY_BUFFER
を使用していることに注意してください。
私たちの立方体を描画するために、 onDisplay
内でOpenGLに指示することができます:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo_cube_elements);
int size; glGetBufferParameteriv(GL_ELEMENT_ARRAY_BUFFER, GL_BUFFER_SIZE, &size);
glDrawElements(GL_TRIANGLES, size/sizeof(GLushort), GL_UNSIGNED_SHORT, 0);
glGetBufferParameteriv
を使用してバッファサイズを取得します。 この方法では、 cube_elements
を宣言する必要はありません。
深度を有効化する
[編集]glEnable(GL_DEPTH_TEST);
//glDepthFunc(GL_LESS);
glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
今、正方形の前面を見ることができますが、キューブの他の面を見るには、それを回転させる必要があります。 前面の三角形の1つ(または両方!)を除去することによって覗き見ることもできます。 :)
Model-View-Projection matrix
[編集]今までは、オブジェクトの座標で作業し、オブジェクトの中心を指定していました。 複数のオブジェクトを組み合わせて、3Dの世界で各オブジェクトを配置するには、変換行列を計算することになり、それには:
- モデル(オブジェクト)座標からワールド座標へのシフト (model->world)
- 次にワールド(カメラ)座標からビュー座標 (world->view)
- 次にビュー座標から投影(2D画面)座標 (view->projection)
また、これはアスペクト比の問題の面倒もみてくれます。
目標は、MVPと呼ばれるグローバル変換行列を計算することで、各頂点にそれを適用することで画面上の最終的な2Dのポイントを取得します。
2Dスクリーン座標は[-1,1]区間であることに注意してください。 これらを[0、screen_size]に変換するために第4のマトリックスでないステップがあり、 glViewPort
によってコントロールします。
歴史上の注意:OpenGL 1.xは、 glMatrixMode(GL_PROJECTION)
と glMatrixMode(GL_MODELVIEW)
を介してアクセス可能な、2つの組み込み行列を持っていました。 ここでは、それらを置き換えて、そのうえでカメラを追加しています:)
私たちのコードにonIdle
関数を追加し、前のチュートリアルの fade
uniform を更新しましょう。 mvp
uniform を代わりに渡します。
Start: 各フェーズの開始時には、単位行列という、まったく変換を行わない行列があり、これは glm::mat4(1.0f)
を使用して作成します。
Model: キューブを少し背景のほうに押して、カメラと混ざらないようにします:
glm::mat4 model = glm::translate(glm::mat4(1.0f), glm::vec3(0.0, 0.0, -4.0));
View: GLM には gluLookAt(eye, center, up)の再実装も用意されています 。eyeはカメラの位置で、centerはカメラが指す場所、upはカメラの上側です(傾いた場合のため)。 少し上方にあるオブジェクトを中心にもってきて、カメラとまっすぐにしてみましょう:
glm::mat4 view = glm::lookAt(glm::vec3(0.0, 2.0, 0.0), glm::vec3(0.0, 0.0, -4.0), glm::vec3(0.0, 1.0, 0.0));
Projection: GLM には gluPerspective(fovy, aspect, zNear, zFar)の再実装も用意されています。 fovyはレンズの角度、aspectは画面のアスペクト比(幅/高さ)、zNearとzFarはクリッピング平面(最小/最大の深さ)で、両方とも正の値、zNearは通常ゼロに等しくはない小さな値です。 見える必要があるのは正方形までなので、zFarには10を使用することができます:
glm::mat4 projection = glm::perspective(45.0f, 1.0f*screen_width/screen_height, 0.1f, 10.0f);
screen_width
と screen_height
はウィンドウのサイズを定義するための新しいグローバル変数です:
/* global */
int screen_width=800, screen_height=600;
/* main */
glutInitWindowSize(screen_width, screen_height);
結果:
glm::mat4 mvp = projection * view * model;
それをシェーダに渡すことができます:
/* Global */
#include <glm/gtc/type_ptr.hpp>
GLint uniform_mvp;
/* init_resources() */
const char* uniform_name;
uniform_name = "mvp";
uniform_mvp = glGetUniformLocation(program, uniform_name);
if (uniform_mvp == -1) {
fprintf(stderr, "Could not bind uniform %s\n", uniform_name);
return 0;
}
/* onIdle() */
glUniformMatrix4fv(uniform_mvp, 1, GL_FALSE, glm::value_ptr(mvp));
そしてシェーダ内で:
uniform mat4 mvp;
void main(void) {
gl_Position = mvp * vec4(coord3d, 1.0);
[...]
アニメーション
[編集]オブジェクトをアニメーションにするために、モデル行列の前に追加の変換をシンプルに適用することができます。
キューブを回転させるために、 onIdle
内に次のものを追加することができます :
float angle = glutGet(GLUT_ELAPSED_TIME) / 1000.0 * 45; // 45° per second
glm::vec3 axis_y(0.0, 1.0, 0.0);
glm::mat4 anim = glm::rotate(glm::mat4(1.0f), angle, axis_y);
[...]
glm::mat4 mvp = projection * view * model * anim;
伝統的な飛行回転キューブができました!
ウィンドウのサイズ変更
[編集]GLUTウィンドウのサイズ決定のサポートには、 glutReshapeFunc
を使用することができます:
void onReshape(int width, int height) {
screen_width = width;
screen_height = height;
glViewport(0, 0, screen_width, screen_height);
}
/* main */
glutReshapeFunc(onReshape);
注:サイズ変更時にシーンがびくつく傾向があります - それがどこに起因するのかは私には解明できませんでした。