跳轉到內容

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;

元素 - 索引緩衝物件 (IBO)

[編輯 | 編輯原始碼]

我們的立方體有 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_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);
  [...]
我們的立方體正在旋轉

為了對物體進行動畫處理,我們只需在模型矩陣之前應用其他變換即可。

為了旋轉立方體,我們可以在 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);


< OpenGL 程式設計

瀏覽並下載 完整程式碼
華夏公益教科書