跳转到内容

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 编程

浏览并下载 完整代码
华夏公益教科书