学习网站 https://learnopengl.com 本人18.1.22学习汇总,方便复习,每一个空行是一个知识点,其中夹杂C++(C++也是刚学),如有不对,请指出,谢谢
温故而知新,可以为师矣。
OpenGL入门复习总结
GL代表Graphics Library图形库,所以OpenGL代表开放图形库。
if(OpenGL新版本){新版本设定} else {OpenGL旧版本设定}。
将int设置为glint的好处是,因为OpenGL是跨平台的,所以glint也是跨平台的。
我们使用所有的对象(各种大的结构体)时,都包含以下几步:
1,定义一个引用id GLuint objectId;
2,创建一个对象绑定到引用上 glGenObject(1, &objectId); //第一个参数是创建一个对象,第二个参数是要绑定的对象id;
3,把对象绑定到环境 glBindObject(GL_WINDOW_TARGET, objectId); //第一个参数是你所需要使用的环境,第二个参数是你要和这个环境绑定的对象。
4,设置当前绑定到GL_WINDOW_TARGET的对象的各种选项,也就是操作OpenGL对象
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_WIDTH, 800);
glSetObjectOption(GL_WINDOW_TARGET, GL_OPTION_WINDOW_HEIGHT, 600);
5,把上下文目标重设为默认 glBindObject(GL_WINDOW_TARGET, 0); //解绑OpenGL对象和环境,将环境的值设置为0。
GLFW是一个C写的专门用于OpenGL开发的库。作用主要是,先创建一个OpenGL环境,然后我们对这篇环境进行我们想要的操作。
OpenGL创建时应该创建一个空项目。导包,然后记得添加链接器。
.dll和.so是动态链接库,可以不把二进制文件包含到本来的文件中。
GLAD是一个开源的库,管理我们所讨论的所有繁琐的工作。GLAD与大多数常见的开源库有一点不同的配置设置。GLAD使用一个Web服务,我们可以告诉GLAD我们要定义哪个版本的OpenGL,并根据该版本加载所有相关的OpenGL函数。
#define MAXTIME 1000
一个简单的MAXTIME就定义好了,它代表1000,如果在程序里面写
if(i
编译器在处理这个代码之前会对MAXTIME进行处理替换为1000。
这样的定义看起来类似于普通的常量定义CONST,但也有着不同,因为define的定义更像是简单的文本替换,而不是作为一个量来使用,这个问题在下面反映的尤为突出。
这段代码#define的作用是,再glew中有一个,如果再调用这个头文件的时候碰到#define GLEW_STATIC的话,就会编译使用#ifdef GLEW_STATIC后面的文件。其他同理。
OpenGL首先需要实例化一个窗口,然后设置OpenGL环境,然后再绑定OpenGL环境,最后设置OpenGL环境,完成渲染。下面是一段设置OpenGL窗口的代码:
glfwInit 初始化glfw。
glfwWindowHint 设置窗口值,然后一直保留此设置,每次设置窗口都会使用这个值,直到调用glfwWindowHint或glfwDefaultWindowHints更改为止,或者直到库终止。
然后调用下面代码创建窗口:
glfwCreateWindow:创建一个窗口,1.2参设置长和高,3参设置名字。该函数返回一个窗口对象。
然后调用glfwMakeContextCurrent(窗口对象指针)将该窗口环境当成我们线程的主环境。
因为需要使用GLAD来管理OpenGL中的函数指针,所以需要在所有渲染操作之前,初始化GLAD。
最后,我们设置我们需要渲染的视口的的大小(视口就是屏幕中的某一块显示区域)。调用glViewport来设置视口。
glViewport(0, 0, 800, 600) 1.2参设置视口的左下角位置,3.4参设置视口的大小。
如果我们想要让这个视口铺满整个窗口屏幕,我们需要将视口和窗口大小设置相同,而当窗口大小改变时,我们必须改变视口的大小。所以,我们需要设置一个回调函数,当窗口大小变化,视口大小也跟着变化。
我们需要设置当每次改变窗口大小的时候,都将调用这个函数。// 即调用回调函数。(所有回调函数都要被设置调用)
注意:在OpenGL中,所有的坐标都是相对于标准坐标(-1,1)之间的,只是将投射到你的屏幕上,变成屏幕的适应大小。
下一步,按帧绘制,每一次循环就是一帧。
glfwWindowShouldClose(创建的窗口对象):检查是否已经指示GLFW关闭,如果是,函数返回true并且游戏循环停止运行,之后我们可以关闭应用程序。
glfwPollEvents():检查是否有任何事件被触发(如键盘输入或鼠标移动事件),更新窗口状态,并调用相应的函数(我们可以通过回调方法设置)。
glfwSwapBuffers(创建的窗口对象):渲染这次迭代,更新屏幕状态。
glfwTerminate():清理删除所有的资源。并且关闭应用程序。
键盘交互回调函数,每次按下某个键的时候,调用这个函数:
glfwGetKey:绑定窗口和键盘按键。 当你按键时候,如果循环没有走到processInput时,就会忽略按键。当循环走到这句话的时候,才会接收按键。然后glfwPollEvents()调用回调函数。
:设置屏幕颜色,glClear清除整个屏幕颜色:GL_COLOR_BUFFER_BIT,清除整个深度缓冲:GL_DEPTH_BUFFER_BIT。
注:回调函数(call_back):回调函数就好像是一个中断处理函数,系统在符合你设定的条件时自动调用。
准备一段现成的代码,调用者可以随时跳转至此段代码的起始地址,执行完后再返回跳转时的后续地址。 CPU为此准备了现成的调用指令,调用时可以压栈保护现场,调用结束后从堆栈中弹出现场地址,以便自动返回。
在回调函数中,往往都需要显示的声明将使用回调函数。如:
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback)。
在OpenGL中每个操作对应的回调函数都不相同。键盘操作对应的调用回调函数的方法是:glfwPollevent();
代码思路:
声明全局变量
声明回调函数(在C++中所有需要被调用的函数都需要在被调用之前就声明,但是可以在被调用之后定义)
Main(){
初始化glfw
建立一个窗口
初始化glad
建立视口
创建各种环境对象(VAO,VBO,EBO,TEXTURE等)
开始循环,判定是否关闭{
刷新屏幕
激活着色器
生成模型
/*
回调函数交互(调用回调函数在哪里写都可以,当它产生这个行为之后,这个回调函数将被调用,但往往是某个对象调用回调函数,只要在这个对象的作用范围内就可以,看完这句话之后我懂了:至于什么时候调用就交由动态库去处理,这样就达到了一个消息通知的效果。这说明回调函数放在哪里时无所谓的,无论放在哪里,什么时候调用它,是由动态库决定的,接收到这个消息之后再调用这个回调函数,和函数位置无关)
即:该回调函数的调用放在这里
和放在这里是没区别的,
// 只要window还没有被消除,就可以使用回调函数。
*/
刷新屏幕缓冲,深度缓冲
}
退出glfw环境
}
示例代码:创建一个窗口。
#include
#include
#include
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow *window);
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;
int main()
{
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "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);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
while (!glfwWindowShouldClose(window))
{
processInput(window);
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}
void processInput(GLFWwindow *window)
{
if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}
在OpenGL中,将3D坐标改变为适合屏幕显示的2D坐标。这一工作由OpenGL来做。
OpenGL的图形输送管道:接收一组3D坐标,然后把它转变为你屏幕上的有色2D像素。
图形输送管道工作步骤:
1,把你的3D坐标转换为适合屏幕显示的2D坐标,
2,是把2D坐标转变为实际的有颜色的像素。
GPU:图形处理器。
着色器:如今大多数显卡都有成千上万的小型处理内核,通过在GPU的每个步骤中运行小程序,在图形管道中快速处理数据。这些小程序被称为着色器。
GLSL:OpenGL着色语言。
图形输送管道各个阶段(每个阶段的输入是上一个阶段的输出):
顶点数据→顶点着色器→(基本图形组装→几何着色器→细分着色器→)(三个可以合三为一:由顶点坐标生成图形)→光栅化(像素化)→像素着色器→测试与混合(处理深度透明度之类)。
顶点数据(VBO,顶点数组对象):包含:1,顶点位置;2,顶点属性(如纹理,颜色等);
顶点着色器:处理顶点位置:将标准空间坐标变化为世界坐标。
基本图形组装:把顶点着色器中表示为基本图形的所有顶点作为输入,然后把所有点组装为 特定的基本图形的形状(生成图形)。
几何着色器:把基本图形的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的 (或是其他的)基本图形来生成其他形状。(完成对大图形的更加细分)。
细分着色器:拥有把给定基本图形细分为更多小基本图形的能力。这样我们就能在物体更接 近玩家的时候通过创建更多的三角形的方式创建出更加平滑的视觉效果。
光栅化:会把基本图形映射为屏幕上相应的像素,生成供像素着色器(fragment shader)使 用的fragment。在像素着色器运行之前,会执行裁切(clipping)。裁切会丢弃超 出你的视图以外的那些像素,来提升执行效率。(将像素化之后多余的栅格裁掉。)
注:OpenGL中的一个fragment是OpenGL渲染一个独立像素所需的所有数据。
像素着色器:主要目的是计算一个像素的最终颜色,这也是OpenGL高级效果产生的地方。 通常,像素着色器包含用来计算像素最终颜色的3D场景的一些数据(比如光 照、阴影、光的颜色等等)。
测试与混合:当所有操作确定,图形也生成了,由顶点数据也构成像素了,然后也渲染了像 素了,将进行alpha测试和混合(blending)。这个阶段检测像素的相应的深 度(和stencil)值,使用这些,来检查这个像素是否在另一个物 体的前面或 后面,如此做到相应取舍。这个阶段也会查看alpha值(alpha值是一个物体 的透明度值)和物体之间的混合(blend)。所以即使在像素着色器中计算出 来了一个像素所输出的颜色,最后的像素颜色在渲染多个三角形的时候也可能 完全不同。
注:因为GPU中没有默认的顶点/像素着色器,所以我们必须定义至少一个顶点着色器和一 个像素着色器,其他用默认的就可以。
注:OpenGL只是在当它们的3个轴(x、y和z)在特定的-1.0到1.0的范围内时才处理3D坐标。所有在这个范围内的坐标叫做标准化设备坐标(normalized device coordinates) 会最终显示在你的屏幕上(所有出了这个范围的都不会显示)。
注:标准化设备坐标接着会变换为屏幕空间坐标(screen-space coordinates),这是使用你 通过glViewport函数提供的数据,进行视口变换(viewport transform)完成的。最后 的屏幕空间坐标被变换为像素输入到像素着色器。
vertex buffer objects, VBO:顶点缓冲对象它会在GPU内存储存大批顶点。使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以无论何处我们都要尝试尽量一次性发送尽可能多的数据。
注:缓冲是显存上的一段空间,它用来储存多种数据。
引用:GLuint VBO; // 设置引用
生成:glGenBuffers(1, &VBO); // 生成VBO对象并绑定引用
绑定缓冲对象:glBindBuffer(GL_ARRAY_BUFFER, VBO); //将VBO绑定到GL_ARRAY_BUFFER这个顶点缓冲对象上(一会将要操作这个顶点缓冲对象)
将数据复制到将要操作的缓冲对象上:glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); // 把用户定义数据复制到当前绑定缓冲的函数:1参,绑定的缓冲对象。2参,希望传递给缓冲的数据的大小(字节为单位)。3参,发送的数据。4参,指定了我们希望显卡如何管理给定的数据。有三种形式:
1,GL_STATIC_DRAW:数据不会或几乎不会改变。
2,GL_DYNAMIC_DRAW:数据会被改变很多。
3,GL_STREAM_DRAW:数据每次绘制时都会改变。
注:以上的过程,我们将顶点数据储存在显卡里,然后使用顶点缓冲对象VBO管理。
顶点着色器:
版本声明:#version 330 core
输入的顶点属性:layout (location = 0) in vec3 position;
gl_Position:预定义位置变量,无论给gl_Position设置成什么,都将变成顶点着色器的输出。
注:要在运行时动态编译着色器源码。
编译着色器:
1,创建着色器对象: GLuint vertexShader; //定义引用ID
vertexShader = glCreateShader(GL_VERTEX_SHADER); // 创建顶点着 色器。1参,要创建的着色器类型(其他着色器同理)
注:像素着色器为:GL_FRAGMENT_SHADER。
2,将源码附加到着色器对象,并编译:
将源码附加到着色器对象:glShaderSource(vertexShader, 1, &vertexShaderSource, null); // 1参,将附加的着色器对象。2参,源码中的字符串数(一长句话也是一个字符串)。 3参,着色器源码。
编译源码:glCompileShader(vertexShader); // 形参是已经附加了源码的着色器对象。
3,查错:
RGBA:计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)元素。缩写为RGBA。
链接着色器:两个着色器被编译出来之后,需要把两个着色器对象链接到一个着色器程序中(shader program)。
着色器程序对象:着色器程序对象(shader program object)是多个着色器最后链接的版本。如果要使用刚才编译的着色器我们必须把它们链接为一个着色器程序对象,然后在渲染物体的时候激活这个着色器程序。激活了的着色器程序之后,在调用渲染函数时着色器才可用。
注:把着色器链接为一个程序就等于把每个着色器的输出链接为下一个着色器的输入。如果你的输出和输入不匹配那么就会得到一个链接错误。
1,创建着色器程序对象: GLuint shaderProgram;
shaderProgram = glCreateProgram();
2,将着色器附加到着色器程序对象上,并用链接器链接:
glAttachShader(shaderProgram, vertexShader); // 附加到着色器程序上
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram); // 链接着色器对象中的着色器
3,查错:
4,激活着色器程序对象: glUseProgram(shaderProgram);
5,使用着色器程序对象开始进行各种渲染:。
6,删除着色器对象:glDeleteShader(vertexShader); //已经添加到着色器程序对象,不再需
glDeleteShader(fragmentShader); // 要着色器对象了。销毁释放。
指定OpenGL如何理解顶点数据(一般放在VBO后面,因为所用的顶点属性是从离它最近的绑定了GL_ARRAY_BUFFER的那个VBO中获取的):
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid * )0);
// (GLvoid*)(3* sizeof(GLfloat)),这个偏移量首先我们要先算出每个float多少字节,然后算出我们要偏移3个float,最后用GLvoid*将偏移量转化为GLvoid*型
// 1参,要配置的顶点属性的location值。2参,顶点属性的大小。3参,数据类型,GL_FLOAT,因为GLSL中vec*是由浮点数组成的。4参,是否希望被标准化。GL_TRUE:所有数据都将被映射到0-1之间。GL_FALSE:不被映射。5参,步长。每个单位顶点属性间的间隙。6参,偏移量。
glEnableVertexAttribArray(0); // 开启location为0的顶点属性。(默认关闭,需要手动开启)
渲染步骤:1,使用一个顶点缓冲对象初始化了一个缓冲中的顶点数据。
2,设置了一个顶点和像素着色器,并绑定到着色器对象。
3,告诉了OpenGL如何把顶点数据链接到顶点着色器的顶点属性上。
VAO:任何随后的顶点属性调用都会储存在这个VAO中。VAO中储存绘制不同物体的所有信 息,当要绘制一个图形(例如VAO中储存了一个三角形),想要画三角形,直接绑定 这个图形的VAO就可以了。(顶点数组对象)
注:OpenGL core-profile版要求我们必须使用VAO,否则OpenGL不知道如何解释我们的顶点输入。如果VAO绑定失败,OpenGL会拒绝绘制任何东西。
绘制顺序: glUseProgram(shaderProgram); // 激活着色器对象
glBindVertexArray(VAO); // 绑定此时需要画的VAO
glDrawArrays(GL_TRIANGLES, 0, 3); // 如何绘制。1参,绘制的图形。2参, 顶点数组起始位置。3参,总共绘制几个顶点。
glBindVertexArray(0); // 解绑VAO
EBO:索引缓冲对象。专门储存索引,OpenGL调用这些顶点的索引来绘制图形。
在EBO中,EBO绑定到GL_ELEMENT_ARRAY_BUFFER上0。
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
绘制时使用:glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // 1参,要绘制的 形状。2参,绘制的顶点数。3参,索引的类型。4参,偏移量。
注:
EBO将也会绑定到VAO上。所以只要绑定VAO,就相当于绑定了这个VAO上的所有操作。
线框模式:glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)
默认模式:glPolygonMode(GL_FRONT_AND_BACK, GL_FILL)
代码思想:设置回调函数
创建顶点着色器并读取源码
创建像素着色器并读取源码
创建着色器程序对象并绑定着色器并且链接着色器(以上封装)
创建VBO,VAO,EBO
绑定VAO
解释顶点数据并绑定到VBO所绑定的缓冲对象
绑定EBO
解绑VAO(单个VAO封装)
循环开始
清屏
激活着色器
绑定VAO
画图
删除VAO,VBO,EBO对象
关闭glfw
着色器:
注:一个向量的元素可以通过vec.x这种方式获取,这里x是指这个向量的第一个元素。你可以分别使用.x、.y、.z和.w来获取它们的第1、2、3、4号元素。
注:我们可以把一个向量作为一个参数传给不同的向量构造函数,以减少参数需求的数量。
注:如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器声明一个同名输入。当名字和类型都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序(program)对象时完成的)。
从CPU往GPU发送数据有两种方式:
1,通过顶点数据发送。
2,通过unifrom发送。
Unifrom中:unifrom是全局变量,不允许声明时初始化。可以在任何地方定义,。Unifrom是const型的,一旦定义无法改变。
Unifrom的值不能更改,但是可以更改unifrom的值所对应的值。即,unifrom = boy。boy将不变,但boy的值将改变。
如果需要改动: glUniform4f( GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"), 0.0f,greenValue, 0.0f,1.0f); // 将设置为变量。然后往变量中赋值。
1参,unifrom对应的引用ID。2,3,4,5参,设定unifrom的属性值。glUniform这种类型的方法中,1值固定是unifrom的引用ID,后面的值是unifrom的值。
glGetUniformLocation(shaderProgram, "ourColor") // 1参,着色器程序对象,2参,unifrom属性值,将返回该属性的unifrom的引用ID。
注:当渲染一个三角形在(rasterization 也译为光栅化)像素化阶段通常生成比原来的顶点更多的像素。
封装着色器类:
代码思想:将源码用文件流.rdbuf()读取到stringsteam数据转化流中。
将stringsteam调用str()转化为string型流
将string流调用c_str()转化为c语言型字符串
创建顶点着色器,像素着色器
顶点和像素着色器和源码绑定
创建着色器程序对象
着色器程序对象和着色器绑定并且链接
销毁着色器对象(绑定时候已经复制了一份)
Use()方法,改变着色器中unifrom型变量的值(本身只声明,没有定义,是个空值)
查错方法
Glsl源码写在文件中,文件后缀随便,约定俗成是.vs和.fs。