本文用于记录总结在学习OpenGL入门部分的思考。
参考的资源有:learnOpenGL,b站傅老师,csdn其他有关文章。
目录
总体渲染管线pipeline理解
渲染的整体流程用简单的语言来描述的话,分为两部分:
1.把描述物体的3D坐标转换成2D坐标(这里面有mvp等变化)
2.把2D坐标转换成有颜色的像素打到屏幕上。(也就是确定屏幕上每个像素点的颜色是什么)
tips:2D坐标和像素有区别,2D坐标是精确的坐标位置,但一个像素只能有一个颜色,如果一个像素里面有多个坐标点?
窗口初始化和渲染循环
窗口初始化
glfwInit();
//设置主要次要版本
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//生成一个名字为learnopengl的800x600的窗口
GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
//检测窗口是否创建
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "failed to initialize GLAD" << std::endl;
return -1;
}
//设置viewpoint尺寸,这个窗口大小也就是画布的大小,之后的那些映射-1到1会投到0到800.
glViewport(0, 0, 800, 600);
//注册窗口大小监听
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
先上代码。我们需要先初始化、然后设置glfw的主次版本和核心模型。然后就是创建window窗口,前两个参数是宽高,第三个是标题。
然后是一个比较关键的代码,glfwMakeContextCurrent(window),因为OpenGL是个状态机,是通过一系列变量来描述OpenGL该如何运行,把window和状态(context)连接,也就是表示,把这个窗口和状态相连。
GLAD是用来管理opengl的函数指针的,在使用函数指针之前需要激活GLAD。
viewport,前两个参数指定视口左下角的位置,后两个表示视口的宽高,可以比前面的窗口大也可以小。
渲染循环
// 渲染循环
while(!glfwWindowShouldClose(window))
{
// 输入
processInput(window);
// 渲染指令
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 检查并调用事件,交换缓冲
glfwPollEvents();
glfwSwapBuffers(window);
}
这里需要有一个概念就是:画面不是静止的,而是在不断渲染重复的过程。所以需要while来进行渲染循环。
输入部分通过processInput()来实现,用于检查是否有外部设备的输入,鼠标、键盘的输入等。
渲染指令:这里只是用一种颜色来填充整个屏幕。
检查事件并交换缓冲:glfwPollEvents函数检查有没有触发什么事件(比如键盘输入、鼠标移动等)、更新窗口状态,并调用对应的回调函数(可以通过回调方法手动设置)。这里需要讲一下双缓冲(一个画面是从左到右、从上到下,一个像素一个像素的画出来的。但是速度没有快到可以一瞬间,在人眼没有反应过来之前就画好。所以需要双缓冲。即:在给你看第一帧画面的同时在画第二帧的画面,画好后把第二帧的画面与第一帧的画面交换swap,循环交替)
VAO/VBO/EBO
这里我先摆出一幅图,后面我会依次来解释这幅图中分别是什么。
现在来分开聊聊各部分是什么
顶点坐标:用来描述物体的一组顶点数据(这里面包含顶点坐标、顶点颜色、纹理坐标等)也就是图中的vertex部分。
如同所示,这里面有三个点的数据,依次为坐标xyz、颜色rgb、纹理坐标st(也叫做uv)。
而通过顶点着色器来识别每部分谁是谁,并分配到后面的部分中。
VBO(顶点缓冲对象):存在CPU中的顶点数组回传递到GPU中,并放进VBO中。(通俗来讲,VBO中的数据就是顶点数组,只不过是在GPU中)
VAO(顶点数组对象):VAO是一个索引模块,用于识别VBO中的数据谁是谁。在VAO中有0-15号位置来存VBO中的数据,例如:0号位置描述的是一个兔子,1号位置描述的是一个狗。也就是说!!在最开始传入VBO中的数据有特别特别多,可以想象我把一个世界的所有坐标都传入了VBO,现在我用VAO来分辨出这组数据中,哪些属于狗,哪些属于猫。
EBO(索引缓冲对象):EBO是用来帮助VAO更好的识别VBO的。可以这么理解:我要画一个正方形,但是在OpenGL的世界中,正方形是由三角形组成的。也就是说我要画2个三角形,也就是需要6个点,但是实际情况是我只需要4个点就行了。(换到那个小世界的例子就是,那些数据点我不可能只用一次,是需要重复利用的)所以需要EBO来描述那些点是一组的。
讲到这里,对于顶点数据的处理就讲完了。现在来讲一下在代码中是如何实现的
//VAO
unsigned int VAO;
glGenVertexArrays(1, &VAO);
glBindVertexArray(VAO);
//VBO
unsigned int VBO;
glGenBuffers(1, &VBO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//EBO
unsigned int EBO;
glGenBuffers(1,&EBO);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
可以看到每个VAO/VBO/EBO都需要创建、绑定,填充数据的。
VAO: 先定义,然后把这个通过Gen这行来生成VAO并把生成的VAO的id传到VAO的这个int中,1表示的生成的VAO个数,后面需要生成多个VAO时改这个参数就行,最后我们还需要把这个生成的VAO塞进pipeline中,通过glBindVertexArray()来实现。
VBO:同样道理,定义,生成、绑定,不过这里的绑定需要注意,使用的时glBindBuffer(),因为是在缓冲区的东西,并要指明绑定在那个位置上GL_ARRAY_BUFFER。之后就是填充数据,将CPU中的数据填充到VBO中,参数分别是(目标缓冲类型、传输数据的大小、需要传输的数据、显卡如何管理数据),最后一项暂时不考虑。
EBO:同理,不过是要绑定在GL_ELEMENT_ARRAY_BUFFER上。
总结一下:整个pipeline就像是个工厂流水线,每个地方的部件是需要先生产然后放到指定的位置才能发挥工作的,也就是说VAO\ABO\EBO是需要定义、生成、绑定、填充。
VertexShader FragmentShader
在前面的部分我们已经在VAOVBOEBO这些东西都做完了,现在需要做的是把pipeline中的顶点着色器和片段着色器设置好。
首先,我们需要有一个概念:顶点着色器片段着色器是相互关联的小程序,他们顶点着色器处理顶点数据然后传递给片段着色器,片段着色器再根据传过来的数据做处理。
所以需要 in out来确定输入输出的数据类型和名称
顶点着色器
//vertexSourse.txt
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
layout (location = 2) in vec2 aTexCoord;
out vec3 ourColor;
out vec2 TexCoord;
uniform mat4 modelMat;
uniform mat4 viewMat;
uniform mat4 projMat;
void main()
{
gl_Position = projMat * viewMat * modelMat * vec4(aPos.x,aPos.y,aPos.z, 1.0f);
ourColor = aColor;
TexCoord = aTexCoord;
}
这里给出一个顶点着色器,可以看到有layout(location=0)in ,为了定义顶点数据改如何管理,我们可以把位置坐标放0号位置,也可以放1号。
而片段着色器需要一个颜色信息来画图,如果片段着色器没有定义颜色的话,就会把物体画成黑色或者白色。
片段着色器
//fragmentSourse.txt
#version 330 core
in vec3 ourColor;
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D ourTexture;
uniform sampler2D ourFace;
uniform float mixValue;
void main()
{
//FragColor = texture(ourTexture,TexCoord);
//FragColor = texture(ourTexture,TexCoord) * texture(ourFace,vec2(1-TexCoord.x,TexCoord.y),0.2);
FragColor = mix(texture(ourTexture,TexCoord),texture(ourFace,TexCoord),mixValue);
}
这里可以看到一个除in out外的特殊变量uniform,这是一个全局变量,是用于我们可以随时改变渲染方式的参数,例如,我们需要不停的变化位置信息,变换纹理、颜色等。
那么该如何使用uniform呢,在渲染循环中,通过glGetUniformLocation查找uniform位置,然后glUniform4f()等函数来修改他。
链接顶点属性
这里插入一条和前面VBO联动的知识点。
在顶点着色器中,我们需要传入顶点的各个信息,那么我们改如何把信息从VBO放到着色器中。!!!有没有想到之前VBO和VAO,我们需要把VBO数据放在VAO的0,1,2号槽位上,而这些槽位就是顶点属性。
//设置顶点属性指针
//位置
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
颜色
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
//纹理坐标
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)(6 * sizeof(float)));
glEnableVertexAttribArray(2);
我们的vertexShader和FragmentShader的源码都是用txt写的,现在需要把txt文件编译然后变成我们熟悉的程序。
最右边是硬盘,里面有着我们写的着色器源码txt文件,但是我们怎么把txt文件传入cpu中呢?
在Shader/cpp和Shader.h中我需要完成的有:在内存中开辟一块buffer,把txt存进去。然后需要stringbuffer来对内存中的文件进行解释(哪个文件是做什么的)。因为OpenGL操作的是char,所以这里面还有个string转成char的过程,以及最重要的编译过程。
这里我就不仔细描述shader.cpp如何写的了,主要注重的是两个txt着色器是如何编写的。
Texture
纹理可以帮助我们让图形变得更加生动,可以想象一个墙纸,把它贴在我们的物体上,这样我们就不需要定义更多的顶点就可以画出一幅图了。
那么首先需要把纹理图片加载到程序中,这里使用的是最流行的stb_image.h库。
对于纹理,我们需要确定的是纹理坐标,也就是每个顶点的颜色应该是纹理图片中的哪个位置,纹理坐标起始于(0, 0),也就是纹理图片的左下角,终始于(1, 1)。例如:一个三角形,我定义了在一个图片中的纹理坐标,那么三角形中的纹理则会在纹理图片中插值得到。
纹理环绕方式/纹理过滤
环绕方式用于处理当纹理坐标超出(0,1)时该取什么样的纹理。
纹理过滤用于处理当近物体和远物体该如何采样的问题。
(这一部分,后面有机会我会仔细写一篇我的理解)
代码实现
//纹理
stbi_set_flip_vertically_on_load(true);
unsigned int TexBufferA;
glGenTextures(1, &TexBufferA);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, TexBufferA);
//环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
int width, height, nrChannel;
unsigned char* data = stbi_load("container.jpg", &width, &height, &nrChannel, 0);
if (data) {
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
glGenerateMipmap(GL_TEXTURE_2D);
}
else {
cout << "load image failed" << endl;
}
stbi_image_free(data);
材质和VBO那些很象,都是先需要生成然后绑定、填充等。不过这里绑定的通道是GL_TEXTURE_2D。然后定义环绕方式、过滤方式。加载需要的纹理图片等。