前言:
虽然C++是个语法及其憨批的语言( 菜是原罪 ),但确实是入门OpenGL这门技术的不二之选。
Tips
顶点数组对象:Vertex Array Object,VAO
顶点缓冲对象:Vertex Buffer Object,VBO
索引缓冲对象:Element Buffer Object,EBO 或Index Buffer Object,IBO
一点点基本的概念
3D坐标转为2D坐标是通过OpenGL的图形渲染管线管理的。
图形渲染管线可以被划分为两个主要部分:
-
第一部分是把3D坐标转换为2D坐标
-
第二部分是把2D坐标转换为实际有颜色的像素
注意:2D坐标和像素不同,2D坐标精准表示一个点在2D空间种的位置,而2D像素是这个点的近似值,2D像素收到你的屏幕分辨率的限制。
管线接受特定的3D坐标,然后把他们转变成屏幕上的有色2D像素输出。图形渲染有很多阶段,每个阶段都是高度专门化的,有一个特定的函数处理。
当今显卡都有很多核心,在GPU上为每一个管线的极端运行各自的小程序,快速处理数据,这些小程序就叫着色器(shader)。OpenGL的着色器是使用着色器语言GLSL。
(蓝色的部分是可以自己写入着色器的部分)Vertex Shader && Fragment Shader 必须写
Vertex Shader:作为单独的定点作为输入。
Fragment Shader:计算一个像素的最终颜色。
顶点输入
标准化设备坐标(Normaliezd Device Coordiantes,NDC)
进过顶点着色器处理过的顶点坐标就是NDC,xyz都在 -1.0 到 1.0 之间。与通常的屏幕坐标不同,(0,0) 是坐标的图像中心。
标准化设备坐标接着会变换为屏幕空间坐标(Screen-space Coordinates),这是使用你通过glViewport函数提供的数据,进行视口变换(Viewport Transform)完成的。所得的屏幕空间坐标又会被变换为片段输入到片段着色器中。
float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.5f, 0.0f };
顶点数组从CPU跨越千山万水到GPU中,将顶点信息原封不动的储存到VBO中。
为了确定各个顶点之间的关系,我们创建了VAO,诠释VBO里面的所有数据具体代表什么。
一般来说一个模型就用一个VAO缓存。
VAO可以有一个Array Buffer一个Element Buffer。
VBO缓存到VAO的Array Buffer。EBO缓存到ELement Buffer。
unsigned int VAO; glGenVertexArrays(1,&VAO); /* 产生1个VAO返还给&VAO,此时可以产生多个,那么上面的VAO就要定义成数组 */ /* 上面的等价于 unsigned int VAO[1]; glGenVertexArrays(1,VAO); 画个三角形用这个搞就有点脱裤子放屁的感觉 */ glBindVertexArray(VAO); /* 以上所有代码,就是相当于给VAO造了一个Array Buffer,然后给绑上去了 */ unsigned int VBO; glGenBuffers(1,&VBO); /* 以上代码生成了一个VBO缓冲 */ glBindBuffer(GL_ARRAY_BUFFER,VBO) //两个参数:1.是要给他绑到哪里 2.传一个buffer的ID /* 以上一行代码就是把刚刚生成的VBO缓冲绑定到GL_ARRAY_BUFFER目标上 */
下面介绍glBufferData()函数。用于把用户定义的数据复制到当前绑定缓冲的函数。
传四个参数
-
第一个参数是Buffer的类型:VAO有两个类型的Buffer,此时我们用的是Array Buffer。
-
第二个参数是具体指明了要缓冲的数据有多少,长度用bytes表达
-
第三个参数是我们具体要发送的数组
-
第四个参数是具体说明了我们的显卡要如何处理我们刚刚发送的数值,有一下三种形式
-
GL_STATIC_DRAW :数据不会或几乎不会改变。
-
GL_DYNAMIC_DRAW:数据会被改变很多。
-
GL_STREAM_DRAW :数据每次绘制时都会改变。
-
glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW); /* 以上代码作用是把之前定义的顶点数据夫复制到缓冲中 */
以上代码建立了顶点数据和VBO之间的联系。因为已经定义了VAO缓冲且绑定了Array Buffer,也就建立了顶点数据与VAO之间的关系。
顶点着色器
#version 330 core layout (location = 0) in vec3 aPos; void main() { gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0); }
下次就是着色器,这次直接用就ok了。这就是个普普通通的顶点着色器。
这次直接硬编码就ok了。(代码里面写代码)
片段着色器
#version 330 core out vec4 FragColor; void main() { FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); }
和顶点着色器一样,直接硬编码。这个就是GLSL着色器语言。
编译着色器
unsigned int vertexShader; vertexShader = glCreateShader(GL_VERTEX_SHADER); //创建着色器 glShaderSource(vertexShader,1,&vertexShaderSource,NULL); //把着色器源码附加到着色器对象上 glCompilesShader(vertexShader); //编译着色器
同理,片段着色器也是这样的流程
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader,1,&fragmentShaderSource,NULL); glCompiledsShader(fragmentShader);
Tips:编译着色器之后可以使用一下代码来看看编译是否成功
int success; string infoLog; glGetShaderiv(vertexShader,GL_COMPILE_STATUS,&success); if(! success){ glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; }
着色器程序
单单只有着色器是远远不够的,要把两个着色器组装成一个着色器程序才能拿出来用。
unsigned int shaderProgram = glCreateProgram(); //创建着色器程序 glAttachShader(shaderProgram,vertexShader); //把之前编译好的shader附加到程序对象上 glAttachShader(shaderProgram,fragmentShader); glLinkProgram(shaderProgram); //最后用glLinkProgram链接他们
就跟着色器的编译一样,我们也可以检测链接着色器是否失败。
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success); if(!success) { glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog); //失败要进行的操作 }
到此为止,我们得到的结果就是一个程序对象,可以调用glUseProgram函数,用刚刚创建的对象作为他的参数,来激活这个对象
glUseProgram(shaderProgram);
在glUseProgram()函数调用后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)
如果不需要的话,就可以delete掉。
glDeleteShader(vertexShader); glDeleteShader(fragmentShader);
到此为止,我们已经把顶点数据发送给了GPU,并且指示着色器处理。
链接顶点属性
前面提到已经建立了顶点数据与VAO之间的联系,但是此时OpenGL还不知道它该如何解释内存中的顶点数据,以及它该如何将顶点数据链接到着色器的属性上。
首先给出代码
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); //开启第0号的Attribute while(!glWindowShouldClose(window)){ processInput(window); glClearColor(0.2f,0.3f,0.3f,1.0f); glClear(GL_COLOR_BUFFER_BIT); glBindVertexArray(VAO); glUseProgram(shaderProgram); glDrawArrays(GL_TRIANGLES,0,3); glfwSwapBuffers(window); glfwPollevents(); }
OK,不出意外的话你现在就可以看到三角形了。( 出意外就再看一遍以上操作 /滑稽 )下面我们来解说代码。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); //链接顶点属性(解析顶点数据):告诉计算机如何读取每个缓存中的数据 glEnableVertexAttribArray(0);
VAO上面有Array Buffer,上面分有n个栏位,就叫Vertex Attribute(顶点特征值)
我们的VBO会被解析为
-
位置数据备战储存为32位(4字节)浮点值
-
每个顶点包含3个这样的值
-
在这3个值之间没有空隙,紧密排列
-
数据中的第一个值在花冲开始的位置
所以我们的 glVertexAttribPointer()函数就会告诉OpenGL该如何解析顶点数据
-
参数1:制定配置的顶点属性,在着色器中,可以把顶点属性的位置值设置为0。
-
参数2:制定顶点属性的大小,由三个值组成,所以为3。
-
参数3:制定数据类型
-
-
参数5:叫做步长,告诉我们连续的顶点属性之间的间隔。
-
参数6: 它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。
此时已经解析好了VAO里面的数据。要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。所以会有以下的代码:
glBindVertexArray(VAO); glUseProgram(shaderProgram); glDrawArrays(GL_TRIANGLES,0,3);
关于glDrawArrays函数:
-
第一个参数:打算绘制的图元的类型
/* GL_POINTS GL_LINES GL_LINE_LOOP GL_LINE_STRIP GL_TRIANGLES GL_TRIANGLE_STRIP GL_TRIANGLE_FAN */
-
第二个参数:顶点数组的起始索引
-
第三个参数: 打算绘制多少个顶点
-
作用: 从一个数据数组中提取数据渲染基本图元
最后祭出完整版代码和沙雕效果图
https://paste.ubuntu.com/p/wBM9XctMny/