🎨 CG | OpenGL 中的立方体变换

​ 在 OpenGL 中,通过矩阵,我们可以对一个物体进行很方便的 3D 变换,使得静态的物体活动起来。这里使用 GLM 来尝试给予一个立方体不同的变换,使其活动起来

🚀 代码: Github

🔗 参考链接:https://learnopengl.com/#!Getting-started/Transformations

画一个立方体

边长为 4, 中心位置为(0, 0, 0)。

分别启动和关闭深度测试 glEnable(GL_DEPTH_TEST) 、 glDisable(GL_DEPTH_TEST) ,查看区别,并分析原因。

画一个立方体比较简单,具体的步骤和之前画三角形差不多

这里使用两个三角形来构成立方体的一个面,具体的顶点数据如下

1
2
3
4
5
6
-0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,

每一行的前三个就是顶点的坐标,后面两个是纹理的位置。可以注意到,其中有两个点是重复的,我们可以使用 EBO 来对其进行简化

删掉重复的点,将其简化为

1
2
3
4
-0.5f, -0.5f, -0.5f,  0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,

并且添加上 EBO

1
2
0, 1, 2,
2, 3, 0,

立方体的其他面也同样的操作,最后得到$46 = 24$个顶点,以及$6 6 =36$个顶点索引

然后用之前的方法对其进行渲染,为了方便观察,这里给他加了一个图片的纹理

1554270129507

这时我们发现,无论是否开启Depth test,都只是显示一个正方形,这是为什么捏?

原来我们是没有使用透视,使用GLM为其加上两个矩阵,将摄像机后移并且加上透视

1
2
3
4
5
6
7
8
9
10
glm::mat4 view = glm::mat4(1.0f);
glm::mat4 projection = glm::mat4(1.0f);

// 将视图后移4个单位
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -4.0f));
// 开启透视视图
projection = glm::perspective(glm::radians(45.0f), (float)this->defaultWidth / (float)this->defaultHeight, 0.1f, 100.0f);

cudeShader->SetMat4("view", view);
cudeShader->SetMat4("projection", projection);

然后再对比一下Depth test

关闭开启
15542703258031554270337630

可以看到,当关闭了Depth test之后,我们可以观察到侧边的四个面,并且不正常地覆盖了正面。这是因为 OpenGL 在渲染的时候并没有计算深度,把侧边的面画在了正面之上。

平移 Translation

使画好的 cube 沿着水平或垂直方向来回移动。

可以使用glm::translate调整model使图像平移

1
2
3
if (this->en_translation) { // 平移
model = glm::translate(model, this->translation);
}

然后加上一些判断条件使其自动进行平移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (this->auto_translation) {
if (dir_t_x == 0) {
translation.x += 0.02f;
if (translation.x > 0.95f) dir_t_x = 1;
}
else {
translation.x -= 0.01f;
if (translation.x < -0.95f) dir_t_x = 0;
}
if (dir_t_y == 0) {
translation.y += 0.01f;
if (translation.y > 0.95f) dir_t_y = 1;
}
else {
translation.y -= 0.03f;
if (translation.y < -0.95f) dir_t_y = 0;
}
}

效果图:

translation

具体的效果就是立方体的位置会不停变化,当 x 或 y 遇到边界的时候就会逆转变化方向,形成上图的弹弹弹效果。

旋转(Rotation)

使画好的 cube 沿着 XoZ 平面的 x=z 轴持续旋转。

这个比较简单,只需要将旋转的xz参数调成 1,那么立方体就会沿着 x=z 轴持续旋转。然后使用glfwGetTime作为旋转角度,然后就可以呈现出秩序旋转的动画了。

1
2
this->rotate = glm::vec3(1.0f, 0.0f, 1.0f);
model = glm::rotate(model, (float)glfwGetTime(), this->rotate);

效果图如下:

rotate

放缩(Scaling)

使画好的 cube 持续放大缩小。

只需要使用gml::scale,其参数分别是xyz轴缩放的比例,然后加上一些判断条件,形成有规律的缩放动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
if (this->en_scale) { // 缩放
model = glm::scale(model, this->scale);
}

if (this->auto_scale) {
if (dir_s_x == 0) {
scale.x += 0.02f;
if (scale.x > 0.9f) dir_s_x = 1;
}
else {
scale.x -= 0.01f;
if (scale.x < 0.1f) dir_s_x = 0;
}
if (dir_s_y == 0) {
scale.y += 0.01f;
if (scale.y > 0.9f) dir_s_y = 1;
}
else {
scale.y -= 0.03f;
if (scale.y < 0.1f) dir_s_y = 0;
}
if (dir_s_z == 0) {
scale.z += 0.02f;
if (scale.z > 0.9f) dir_s_z = 1;
}
else {
scale.z -= 0.03f;
if (scale.z < 0.1f) dir_s_z = 0;
}
}

效果如下:

scale

添加 GUI

GUI 和之前的也差不多,使用Checkbox激活相应的变换以及动画,使用SliderFloat手动调整或呈现变换的参数。

1554282469505

这样就可以同时控制多个变量,调整参数快速得到我们需要的结果

渲染管线

OpenGL 中的图形渲染管线(Graphics Pipeline),是原始数据转换成屏幕上的像素的一个过程。

其主要可以分为两个部分,首先是将 3D 坐标转换成 2D 坐标,然后就将 2D 坐标转换成屏幕上的像素。

着色器(Shader)就是在管线中的处理程序。着色器是运行在 GPU 的程序,支持并行化处理。我们平时用到的有顶点着色器和片段着色器。

在一个完整的渲染管线中,首先我们得顶点数据将会被送到顶点着色器,顶点着色器负责解析顶点数据。

就拿这次的实验的顶点着色器为例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#version 450 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 TexCoord;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0f);
TexCoord = vec2(aTexCoord.x, aTexCoord.y);
}

首先,将顶点数据分为两层(location),前三个为 3D 坐标,后两个为纹理位置。

然后有三个uniform数据,是从 CPU 中传递进来的数据。

还有一个out为输入到其他着色器的数据

GPU 会将传输进来的数据根据main函数对其进行处理,输出gl_PositionTexCoord两个变量到下一个过程。

然后在片段着色器中对上个着色器输出的TexCoord进行处理,并输出。

1
2
3
4
5
6
7
8
9
10
#version 450 core
out vec4 FragColor;

in vec2 TexCoord;

uniform sampler2D woodTexture;

void main() {
FragColor = texture(woodTexture, TexCoord);
}

而在这两个着色器中间其实还有一些着色器,比如几何着色器。不过这个应该是通过glDrawArrays(GL_TRIANGLES, 0, 36);选择相应的图元(如 GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP)生成的。

其一般的流程如下图:

img

而在我们这几次的实验中,只用到了顶点着色器和片段着色器以及指定一下图元。

将以上三种变换相结合

这里再弄一个立方体,使其围绕着中间的旋转

只需要使用glm的函数对model进行多个变换就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (en_other) {
model = glm::mat4(1.0f);
if (this->en_translation) { // 跟随大立方体平移
model = glm::translate(model, this->translation);
}
// 变成小立方体
model = glm::scale(model, glm::vec3(0.2f, 0.2f, 0.2f));
// 绕着大立方体旋转
model = glm::rotate(model, (float)glfwGetTime(), glm::vec3(0.0f, 1.0f, 0.0f));
// 给上面的旋转一个半径,形成公转
model = glm::translate(model, glm::vec3(5.0f, 0.0f, 0.0f));
// 自转
model = glm::rotate(model, (float)glfwGetTime(), glm::vec3(0.0f, 1.0f, 1.0f));
cudeShader->SetMat4("model", model);
// 绘制
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
}

具体效果如下:

other

当然可以添加上一开始弄得碰撞平移动画,把他叠加上面,就可以边公转边自转然后还能平移

others

这样看上去依旧又掉鬼畜,因此,我把摄像机向上移动了一点

1
2
3
view = glm::lookAt(glm::vec3(0.0f, 2.0f, -6.0f),
glm::vec3(0.0f, 0.0f, 0.0f),
glm::vec3(0.0f, 1.0f, 0.0f));

然后把他们的旋转参数调成(0.0f, 1.0f, 0.0f),使其绕着 y 轴旋转

然后再加上一个小的“月球”绕着地球旋转。

只需要在地球的基础上加上相同的参数即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (en_other) {
model = glm::mat4(1.0f);
if (this->en_translation) { // 平移
model = glm::translate(model, this->translation);
}
model = glm::scale(model, glm::vec3(0.2f, 0.2f, 0.2f));
model = glm::rotate(model, (float)glfwGetTime(), glm::vec3(0.0f, 1.0f, 0.0f));
model = glm::translate(model, glm::vec3(5.0f, 0.0f, 0.0f));
model = glm::rotate(model, (float)glfwGetTime(), glm::vec3(0.0f, 1.0f, 0.0f));
cudeShader->SetMat4("model", model);
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);

model = glm::scale(model, glm::vec3(0.2f, 0.2f, 0.2f));
model = glm::rotate(model, (float)glfwGetTime(), glm::vec3(0.0f, 1.0f, 0.0f));
model = glm::translate(model, glm::vec3(5.0f, 0.0f, 0.0f));
model = glm::rotate(model, (float)glfwGetTime(), glm::vec3(0.0f, 1.0f, 0.0f));
cudeShader->SetMat4("model", model);
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);
}

最终效果如下:

more

土豪通道
0%