OpenGL編程/現代OpenGL教程 05
我們的三角形動畫是挺有趣,但是我們學習OpenGL是爲了看3D圖像。
來創建一個立方體吧!
增加第4個維度
編輯立方體是在3D空間中的8個頂點(4個在前面,4個在後面)構成的。
triangle
可以被重命名成cube
。
同樣記得註釋掉fade
的綁定(bindings)。
現在來寫立方體頂點吧。我們會像圖中一樣放置我們的(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;
元素——索引緩衝區對象(IBO)
編輯立方體有6個面。 某兩個面可能會共享一些頂點。另外,我們會將面寫成2個三角形的結合物(所以一共是12個三角形)。
接下來,我們來介紹一下元素的概念:我們使用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
1, 5, 6,
6, 2, 1,
// back
7, 6, 5,
5, 4, 7,
// bottom
4, 0, 3,
3, 7, 4,
// left
4, 5, 1,
1, 0, 4,
// right
3, 2, 6,
6, 7, 3,
};
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);
注意我們又使用了一個緩衝對象,不過這裏是GL_ELEMENT_ARRAY_BUFFER
而不是GL_ARRAY_BUFFER
。
可以告訴OpenGL來繪製我們的立方體了,就在render
中:
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);
我們現在可以看到方形正面了,但是爲了看到立方體的其他面,我們需要旋轉它。 我們還可以通過刪除正面三角形中的一個(或兩個!)來窺視(peek)一下。 :)
Model-View-Projection矩陣
編輯到目前爲止,我們已經有了物體座標——繞物體的中心來指定。爲了可以有多個物體並且在3D世界中放置每一個,我們要像這樣計算一個變換矩陣:
- 由模型(物體)的坐標變換到世界坐標(model->world)
- 然後從世界坐標到視(攝影機)坐標(world->view)
- 再然後從視坐標到投影(2D屏幕)坐標(view->projection)
這也會同時解決外觀比例的問題。
我們的目標是計算全局變換矩陣,稱爲MVP。我們會將它應用到每個頂點以得到最終顯示在屏幕上的2D點。
注意2D屏幕坐標全都在[-1,1]區間內。還有個不使用矩陣的第4步以將他們轉換到[0, 屏幕尺寸]——由glViewPort
控制。
關於歷史的告示:OpenGL 1.x擁有兩個內置矩陣,可以通過glMatrixMode(GL_PROJECTION)
和glMatrixMode(GL_MODELVIEW)
訪問。在這裏我們要取代它們,另外我們要增加一個攝影機 :)
把我們的代碼加到logic
函數中,就在我們於前一個教程更新fade
律態的地方。我們會傳遞一個mvp
律態作爲代替。
開始:在每一相位開始處,我們有一個單位矩陣(不帶來任何變換),使用glm::mat4(1.0f)
創建。
模型:我們會把我們的立方體稍微推一點(在背景中),這樣它不會和攝影機相混:
glm::mat4 model = glm::translate(glm::mat4(1.0f), glm::vec3(0.0, 0.0, -4.0));
視圖: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));
投影:GLM也同樣提供了對gluPerspective(fovy, aspect, zNear, zFar)的重新實現:aspect是屏幕的外觀比例(寬/長);fovy是view的縱場(vertical field)(對於一個4:3解析度(resolution)之中的common 60° horizontal FOV來說是45°);zNear和zFar是剪裁平面(clipping plane)(最小/最大深度)——都是正數——而且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 */
SDL_Window* window = SDL_CreateWindow("My Textured Cube",
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
screen_width, screen_height,
SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL);
結果:
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;
}
/* logic() */
glUniformMatrix4fv(uniform_mvp, 1, GL_FALSE, glm::value_ptr(mvp));
以及在着色器中:
uniform mat4 mvp;
void main(void) {
gl_Position = mvp * vec4(coord3d, 1.0);
[...]
動畫
編輯爲了讓物體動起來,我們可以簡單地在模型(Model)矩陣之前應用額外的變換。
爲了旋轉立方體,我們可以在logic
中增加這些:
float angle = SDL_GetTicks() / 1000.0 * 45; // 45° per second
glm::vec3 axis_y(0, 1, 0);
glm::mat4 anim = glm::rotate(glm::mat4(1.0f), glm::radians(angle), axis_y);
[...]
glm::mat4 mvp = projection * view * model * anim;
我們這就做出了傳統的飛行旋轉立方體!
窗口大小調整
編輯爲了支持對該SDL2窗口的縮放,你可以檢視(那些)SDL_WINDOWEVENT
:
void onResize(int width, int height) {
screen_width = width;
screen_height = height;
glViewport(0, 0, screen_width, screen_height);
}
/* mainLoop */
if (ev.type == SDL_WINDOWEVENT && ev.window.event == SDL_WINDOWEVENT_SIZE_CHANGED)
onResize(ev.window.data1, ev.window.data2);
注意:場景在縮放的時候會(tend to)變得跳躍(jumpy)——我無法推斷出它從何而來。