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)——我无法推断出它从何而来。