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;
我们的立方体有 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_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);
[...]
为了对物体进行动画处理,我们只需在模型矩阵之前应用其他变换即可。
为了旋转立方体,我们可以在 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);