首先,给出这次学习的代码原网址。------>原作者的源代码 (黑体是源码,注释是写的。) 引用的库(预编译):
#include <glad/glad.h> //控制编译时函数的具体位置的库,GLAD是用来管理OpenGL的函数指针。
//因为各个计算机显卡驱动版本不同,所以需要在编译的时候现场确定位置。
#include <GLFW/glfw3.h>
//OpenGL的c语言实现库,提供了一些必备的函数接口。
#include <iostream> //c++的输入输出流库
#include <cmath>//是一个数学函数库,等同c语言的math.h
自定义的函数声明及全局变量:
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
//用户改变窗口的大小的时候,视口也应该被调整,这个函数会在每次帧缓冲中,
//传入窗口的大小来设置视口(也即是渲染的大小),这就是一个回调函数
void processInput(GLFWwindow *window);
//这个函数名的翻译是:加工输入对象。
//这个函数是用来容纳输入控制,在程序中,我们想做出一些诸如按键,点击,来让
程序作出一些反应。(这里例子设置为按esc键,窗口渲染结束,退出。)
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
//设置窗口宽800,高600
着色器源代码:
//这是一个顶点着色器(Vertex Shader)的代码,这个着色器允许我们自己编写,
它的功能是将数学的坐标数据(输入),转换为点并可以进行额外的处理,变成点
给下面的形状图元装配,几何着色器。
const char *vertexShaderSource =
"#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
" gl_Position = vec4(aPos, 1.0);\n"
"}\0";
//这段代码的写法很奇怪,这段代码对应的机器码是运行在GPU,存储GPU附近的
寄存器里。语言是一种叫GLSL的着色器语言本身这段代码的编译的时候只是一段
字符串常量,在运行的时候再进行编译进入GPU中运行。
//首先需要有一个版本声明,这个与你用的opengl版本一致,还有声明使用“核心模式”
然后是一个关键字in,用来声明所有的输入顶点属性。现在我们只关心位置数据,
所以我们只需要一个顶点属性,也就是一个(x,y,z)。这里我们用vec3这个向量
类型作为输入。用gl_Position(vec4)作为输出.这里有一个
layout (location = 0)这个设置了输入变量的位置值,主要是用来确定
aPos这个值的位置在哪里,类似一个句柄(?暂时还不理解这个)
//这是一个片段着色器,用处是将光栅化的一个个空白像素,填上颜色
const char *fragmentShaderSource = "#version 330 core\n"
"out vec4 FragColor;\n"
"uniform vec4 ourColor;\n"
"void main()\n"
"{\n"
" FragColor = ourColor;\n"
"}\n\0";
关于两个自定义函数的实现:
void processInput(GLFWwindow *window) //处理输入函数
{ if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS) glfwSetWindowShouldClose(window, true);
}
//首先传入我们的窗口对象,明确是针对哪个窗口而言
//glfwGetKey函数检查esc是否被按下,被按下返回GLFW_RELEASE,执行条件体。
将该窗口的WindowShouldClose属性设置成ture,来关闭窗口。
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{ glViewport(0, 0, width, height);
}
//窗口大小的回调函数,每次渲染时都会,调用传入窗口的宽,高,来重新设置视口的大小
int main()函数内部各个部分分析: (1)初始化glfw:
glfwInit(); //初始化glfw库,这在调用大部分其它GLFW函数前,都需要初始化GLFW
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
//设置版本,设置核心模式。(Hint直接翻译成中文是“线索”,这里大致指“选项”。)
(2)创建窗口对象(窗口对象存放了所有和窗口相关的数据,会不断被调用):
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);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
//glfwCreateWindow函数,传入宽,高,还有标题,还有两个参数暂时不用。
//glfwMakeContextCurrent通知GLFW将窗口的上下文设置为当前线程的主上下文,
这样下一个渲染时刻,这个就被渲染出来了。
//glfwSetFramebufferSizeCallback是注册回调函数,后面参数是想要回调的函数名,来让每次渲染该函数都被调用。
(3)初始化GLAD(设定正确的函数指针):
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
//glfwGetProcAddress是glfw提供的系统相关的OpenGL函数指针地址的函数,做了一个
(GLADloadproc)类型转换,用gladLoadGLLoader函数根据每个人的系统定义了正确的函数
(4)渲染循环while:
while(!glfwWindowShouldClose(window)) {
glfwSwapBuffers(window);
glfwPollEvents();
}
//glfwWindowShouldClose函数在我们每次循环的开始前检查一次GLFW是否被要求退出
//glfwSwapBuffers函数会交换颜色缓冲,它在这一渲染迭代中被绘制,作为输出显示在屏幕上。
//glfwPollEvents函数检查有没有触发什么事件,并调用对应的回调函数(需要注册在window对象上)
渲染循环简单可以分为三步: 1.输入 2.渲染指令3.检查并调用事件,交换缓冲 (5)正确释放之前的分配的所有资源:
glfwTerminate();//该函数释放所以分配内存空间
return 0;
(6)每次渲染的清屏函数:
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
//glClear函数来清空屏幕的颜色缓冲,它接受一个缓冲位(Buffer Bit)来指定要清空的缓冲,我们这里只清空颜色,不清空其他的。
//glClearColor来设置清空屏幕所用的颜色,注意glClearColor函数是一个状态设置函数,本身不做清除,而是设置glClear函数,而glClear函数则是一个状态使用的函数,它使用了当前的状态来获取应该用什么颜色替换之前的颜色。
(7)运行时动态编译着色器:
//我们现在已经写了一个着色器源码(放在一个字符串,用一个指针指向了它),现在我们创建着色器对象(用一个int变量作为引用来指向着色器所在的内存空间)。再将源码传进去,在创建对象之后,在运行时进行动态编译。
int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
//glCreateShader函数用来创建对象。参数GL_VERTEX_SHADER用来告诉函数创建的时一个顶点着色器。
//glShaderSource函数,用来将源代码绑定在创建对象上(注意这里用的是引用),1表示传入的源代码字符串数量,第三个参数是顶点着色器真正的源码所在的全局变量指针,(?这里为什么要传入指针本身的地址,而不是指针指向的字符串的地址呢)
//glCompileShader(),被调用的时候,就会按照先前绑定的情况进行绑定,这里也是设置函数与使用函数的区别。
----------------检测glCompileShader编译是否成功,错误返回错误原因-------------
int success;//定义一个整型变量来表示是否成功编译
char infoLog[512]; //储存错误消息
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n"
<< infoLog << std::endl;
} //glGetShaderiv检查是否编译成功。如果编译失败则调用glGetShaderInfoLog获取错误消息,并且打印 //片段着色器编译过程类似。
(8)连接着色器制作着色器程序对象:
//之前我们定义了着色器源码,并且设定了如何去动态编译,但是着色器工作是六个着色器按照固定顺序共同完成的,单独一个着色器并没有办法使用,所以我们需要定义一个着色器对象,用来连接并容纳我们自己定义的着色器,当然glfw库本身也提供剩下的着色器,来完成着色工作
int shaderProgram = glCreateProgram(); //依旧是用一个句柄来引用着色器程序对象
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader); //附加
glLinkProgram(shaderProgram);//特定顺序链接
//glAttachShader函数把之前编译的着色器附加到程序对象上,之后glLinkProgram函数会把他们链接起来,这是按照一个特定的顺序链接的,这一步程序会自动帮助我们完成。
------------------检测glLinkProgram链接是否成功,并返回错误原因-------------------
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
} //这里使用的是glGetProgramiv,glGetProgramInfoLog这两个函数它们跟上面两个类似,就是传入的不是shader而是program。
glUseProgram(shaderProgram);//将这段函数加入while循环函数的渲染部分,就可以激活这个着色器程序对象了。
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
//把着色器对象链接到程序对象以后,可以删除着色器对象,因为程序对象已经有了,着色器已经没有使用的机会了,占用的内存空间可以释放了.
(9)输入顶点数据:
//我们的着色器程序对象需要一个输入对象(这里我们只给它位置坐标),然后经过六个着色器处理输出成像素数据,在传给硬件(显示器),变成我们看到的图案。
float vertices[] = {
0.5f, -0.5f, 0.0f, // bottom right
-0.5f, -0.5f, 0.0f, // bottom left
0.0f, 0.5f, 0.0f // top
};
//我们这里定义一个float的数组,f表示是单精度浮点数,注意本质上这就是九个浮点数连续排列,我们还需要告诉程序,按照三个三个去读取,每三个中第一个是x,第二个是y,第三个是z。
//这三个点的坐标数据,会被顶点着色器放在GPU的内存(也叫做显存)中,接下来我们要管理这个内存,因为很多时候我们需要从CPU往GPU发送大量的数据,并且不断的访问这些顶点的内存位置,这些内存不一定在物理上是连续的,而且CPU往GPU发送速度太慢,最好一次发送足够的数据。所以我们需要一个对象,一个顶点缓冲对象来管理,容纳这些内存位置,这样一方面我们可以利用它往GPU发送数据,一方面我们将很多的顶点的内存位置转为了一个顶点缓冲对象,方便去管理,使用。
//生成顶点缓冲对象,用来管理所以顶点数据所在显存
unsigned int VBO;
glGenBuffers(1, &VBO);//这是生成缓冲区对象
glBindBuffer(GL_ARRAY_BUFFER, VBO);//这是设置缓冲区对象的类型,这里设置为顶点缓冲对象GL_ARRAY_BUFFER
//"从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。"
关于这两个函数,有一个博文提到缓冲区函数的区别--->glGenBuffers与glBindBuffer理解
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//把之前定义的顶点数据复制到缓冲的内存,第四个参数指定了我们希望显卡如何管理给定的数据,GL_STATIC_DRAW :数据不会或几乎不会改变。GL_DYNAMIC_DRAW:数据会被改变很多。GL_STREAM_DRAW :数据每次绘制时都会改变。
(10)链接顶点属性:
//我们必须手动指定输入数据的哪一个部分对应顶点着色器的哪一个顶点属性,opengl不会自己替我们做这件事。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0);
//使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据(应用到逐个顶点属性上)。第一个参数顶点属性,0是layout(location = 0)中对aPos的位置,这里传入0,表示我们想让数据传递到0代表的顶点属性中。第二个是顶点属性的大小,vec3的大小是3,第三个是指定数据类型,这里是浮点数
。第四个参数是是否希望数据被标准化,我们传入的已经标准了不需要。第五个参数是步长,这个已经指定了3 * sizeof(float),这里也可以是0,让程序自己决定是多少,这个只有在数据紧密排列才可以使用。第六个参数是位置数据在缓冲中起始位置的偏移量,这里是0.
//glEnableVertexAttribArray函数是用来启用顶点属性,也就是将顶点数据链接到着色器的顶点属性上,因为这一项默认是禁用的,我们需要开启。
(11)顶点数组对象: