从零开始写3D游戏引擎(开发环境VS2022+OpenGL)之九点五 编写运动摄像机镜头的源代码 细嚼慢咽逐条读代码系列
本篇博主金沙阳新开的逐条读代码系列,帮助编程初学者理解3D编程的奥妙之处,如有不周请多多指教。
[!TIP]
本系列的姊妹篇为从零开始写3D游戏引擎(开发环境VS2022+OpenGL)之八点五 细嚼慢咽3D引擎的代码实现-CSDN博客
本篇文章所涉及的源代码可以在这个网页链接找到:可移动镜头中的飞翔的黄金条纹理的立方体资源-CSDN文库
项目要实现的目标
今天项目目标是实现对3D模型的查看。用户可以以自身视觉作为摄像系统,翻转整个模型。这里的模型就是画着金条纹理的立方体。
实现的效果如下图:
显然,这里面涉及到了3D模型的渲染,具体的实现就是使用OpenGL库。
代码文件目录
下面说一下代码和资源文件有哪些。
如果您从本博文给出的资源处下载的话,一共可以找到7个文件。
其中GoldBar.cpp文件是主程序文件,整个程序的主体都在其中。
三个头文件shader_m.h,stb_image.h以及camera.h文件,分别是着色器相关的头文件,图像处理相关的头文件以及镜头处理相关的头文件。
文件7.4.camera.fs 是片段着色器的文件,
文件7.4.camera.vs是顶点着色器的文件。
这两者都可以被着色器类中的函数处理,从而让GPU编译。
最后一个是纹理图片GoldBars0.jpg。这个就是立方体表面的金条的图片。
格式是jpg。这个最好不要换掉。
下面就开始逐行讲解代码了。
首先讲的是,顶点着色器的待编译文件7.4.camera.vs
7.4.camera.vs
文件内容如下:
#version 330 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);
}
这个文件,是对象化处理顶点着色器的输入文件。使用的语言是着色器语言GLSL。
#version 330 core
第一行代码,指定了OpenGL的版本号,这里用的是3.3版本。不同版本用的特性也不一样。现在最新的版本是4.6,如果你的OpenGL也是4.6版本,你可以将"#version 330 core"替换成"#version 460 core"。当然不同版本的GLSL对应着不同的OpenGL规范,但是一般有较好的对下兼容功能,毕竟不是所有人都有最新的RTX显卡。
第二行与第三行代码:
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;
前者表示位置变量的属性位置值为 0,后者表示纹理变量的属性位置值为 1。
[!TIP]
位置变量aPos是一个三维向量,类型名为vec3,对应着一个xyz笛卡尔坐标轴上的点。
纹理变量aTexCoord是一个二维向量,类型名为vec2,对应着一个二维的纹理。
这个时候我们就要跳跃到主文件中对应部分了,在主文件GoldBar.cpp中,给出了顶点的数据:
...
float vertices[] = {
-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,
...
}
...
这里,每一行的前三个数值,代表的就是位置坐标。
后两个数值,代表的是如何将纹理图片给映射上去。(0 0)表示左下,(1 1)表示右上,其余的点,你可以类推。
之所以会有位置值0和1,这是因为,GPU得到的数据为下面这样的组成。
当然,绿色的部分为颜色,在这里你可以将他们忽略,红色和蓝色的部分,分别是坐标(XYZ)和这个顶点对应着的纹理图案的位置(ST)
继续回到顶点着色器代码部分。
layout 后面有个关键词“in”,表示这两个变量aPos和aTexCoord是输入变量,由主程序向GPU顶点着色器小程序发送。
接着看:
out vec2 TexCoord;
这里的关键词 out,表示这个二维变量TexCoord(也就是渲染好的纹理变量)将作为输出,用于新的着色器小程序,也就是我们后面要提到的片段着色器。
这里我们就可以看出来:虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了in
和out
关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同。
[!TIP]
这也是为什么渲染过程被称为管线渲染(render pipeline),整个渲染的过程是一环接着一环的,就像一个水管(pipeline)一样。
当然,我们编程的时候只会聚焦我们能够控制的部分,光栅化之类的步骤,因为是在GPU内部自动完成的,所以我们不会去具体的讨论。
再下面就是三个uniform 类变量了。
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
这三个变量 model view以及projection 都是mat4类型的变量,也就是4维矩阵变量。从英文名也可以知道这三个变量的含义,分别是模型(也就是图形里面的立方体),视角(也就是摄像头或者说相机的朝向矩阵)以及投影(也就是我们在镜头中应该看到的部分,窗口划定的部分。)
uniform 类变量,说明这三个变量是全局性的,可以在整个主代码中修改。
毕竟从逻辑上来说,当我们转动鼠标,或者敲击键盘wasd,我们希望摄像头都会出现对应的转动,上下左右以及前后移动。
所以这里用uniform类来声明这个变量再合适不过。
接下来的部分,就是如何处理输入和输出了。
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0f);
TexCoord = vec2(aTexCoord.x, aTexCoord.y);
}
GLSL语言就是写给GPU的C语言。只不过是特化的C语言。
gl_Position是默认是归一化的裁剪空间坐标,xyz各个维度的范围为-1到1,仅能在顶点着色器中使用,既是输入也是输出。gl_Position赋值范围就是float的取值范围(32位),只不过只有[-1,1]区间的片元被绘制。它是vec4类型的,不能重声明为dvec4等类型。
因为一个顶点坐标将会根据以下过程被变换到裁剪坐标:
V c l i p = M p r o j e c t i o n ⋅ M v i e w ⋅ M m o d e l ⋅ V l o c a l V_{clip}=M_{projection}⋅M_{view}⋅M_{model}⋅V_{local} Vclip