OpenGL编程/现代OpenGL教程 05
我们的三角形动画是挺有趣,但是我们学习OpenGL是为了看3D图像。
来创建一个立方体吧!
增加第4个维度
[编辑]立方体是在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_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);
[...]
动画
[编辑]为了让物体动起来,我们可以简单地在模型(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)——我无法推断出它从何而来。