OpenGL 程式設計/現代 OpenGL 教程 05
我們的三角形動畫很有趣,但我們學習 OpenGL 是為了檢視 3D 圖形。
讓我們建立一個立方體!

一個立方體是 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;
我們的立方體有 6 個面。兩個面可以共享一些頂點。此外,我們將把我們的面寫成 2 個三角形的組合(因此總共 12 個三角形)。
因此我們將介紹元素的概念:我們使用 glDrawElements,而不是 glDrawArrays。它接受一組引用頂點陣列的索引。使用 glDrawElements,我們可以指定任何順序,甚至可以多次指定同一個頂點。我們將把這些索引儲存在索引緩衝物件 (IBO) 中。
最好以類似的方式指定所有面,這裡為逆時針方向,因為這對於紋理對映(參見下一教程)和光照(因此三角形法線需要指向正確方向)很重要。
/* Global */
GLuint ibo_cube_elements;
/* init_resources */
GLushort cube_elements[] = {
// front
0, 1, 2,
2, 3, 0,
// right
1, 5, 6,
6, 2, 1,
// back
7, 6, 5,
5, 4, 7,
// left
4, 0, 3,
3, 7, 4,
// bottom
4, 5, 1,
1, 0, 4,
// top
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);
現在我們可以看到正方形的前面,但為了看到立方體的其他面,我們需要旋轉它。我們仍然可以透過刪除前面其中一個(或兩個!)三角形來窺視。:)
到目前為止,我們一直在使用物體座標,這些座標是在物體中心周圍指定的。為了處理多個物體並定位 3D 世界中的每個物體,我們計算一個變換矩陣,該矩陣將
- 從模型(物體)座標移到世界座標(模型->世界)
- 然後從世界座標移到檢視(攝像機)座標(世界->檢視)
- 然後從檢視座標移到投影(2D 螢幕)座標(檢視->投影)
這也會解決我們的縱橫比問題。
目標是計算一個全域性變換矩陣,稱為 MVP,我們將將其應用於每個頂點以獲得螢幕上的最終 2D 點。
請注意,2D 螢幕座標位於 [-1,1] 區間內。還有一個非矩陣步驟來將這些座標轉換為 [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 是垂直視野(45° 用於 4:3 解析度下的 常見的 60° 水平 FOV)可以透過以與更改縱橫比軸約束相同的方式重新計算視野來縮放,zNear 和 zFar 是裁剪平面(最小/最大深度),兩者都為正數,zNear 通常很小,不等於零。我們需要看到我們的正方形,因此我們可以使用 10 作為 zFar
glm::mat4 projection = glm::perspective(recalculatefov(), 1.0f * screen_width / screen_height, 0.1f, 10.0f);
float recalculatefov()
{
return 2.0f * glm::atan(glm::tan(glm::radians(45.0f / 2.0f)) / aspectaxis());
}
float aspectaxis()
{
float outputzoom = 1.0f;
float aspectorigin = 16.0f / 9.0f;
int aspectconstraint = 1;
switch (aspectconstraint)
{
case 1:
if ((screen_width / screen_height) < aspectorigin)
{
outputzoom *= (((float)screen_width / screen_height) / aspectorigin)
}
else
{
outputzoom *= ((float)aspectorigin / aspectorigin)
}
break;
case 2:
outputzoom *= (((float)screen_width / screen_height) / aspectorigin)
break;
default:
outputzoom *= ((float)aspectorigin / aspectorigin)
}
return outputzoom;
}
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);
[...]

為了對物體進行動畫處理,我們只需在模型矩陣之前應用其他變換即可。
為了旋轉立方體,我們可以在 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);