OpenGL編程/現代OpenGL教程 05

维基教科书,自由的教学读本

我們的三角形動畫是挺有趣,但是我們學習OpenGL是爲了看3D圖像。

來創建一個立方體吧!

增加第4個維度[编辑]

Coordinate system

立方體是在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_widthscreen_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);
  [...]

動畫[编辑]

Our cube, rotating

爲了讓物體動起來,我們可以簡單地在模型(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)——我無法推斷出它從何而來。