夏天/isummer

Sun of my life !Talk is cheap, Show me the code! 追风赶月莫停留,平芜尽处是春山~

博客园 首页 新随笔 联系 管理

OpenGL开发中,着色器(Shader)是用于控制图形渲染管线各个阶段的小程序。它们是用GLSL(OpenGL Shading Language)语言编写的,GLSL是一种类似于C的语言。着色器在GPU上执行,负责处理顶点、片段等数据,最终生成图像。

 参考:

  Opengl各种函数说明:https://docs.gl/gl3/glVertexAttribPointer

  渲染管线 顶点着色器 片元着色器 VAO VBO :  https://blog.csdn.net/Wang_Dou_Dou_/article/details/120378218

  【QOpenGL】VAO、VBO以及顶点着色器和片段着色器的实现:  https://blog.csdn.net/Antonio915/article/details/147876349

 

1、什么是着色器?

着色器(Shader)是运行在 GPU 上的小程序,用于处理图形渲染过程中的特定任务。传统的 OpenGL 渲染流程中,CPU 需要承担大量的图形计算任务,而引入着色器后,将这些计算任务转移到 GPU 上,利用 GPU 的并行计算能力,大大提高了渲染效率。

工作原理:

当 OpenGL 进行图形渲染时,会将顶点数据(如顶点坐标、颜色、纹理坐标等)传递给着色器进行处理。着色器根据预设的算法对这些数据进行计算和转换,最终生成像素的颜色值,用于显示在屏幕上。整个渲染过程可以分为多个阶段,每个阶段由不同类型的着色器负责处理。

2. opengl图像渲染管线(Graphics Pipeline):

  现代3D图像编程会使用管线的概念。在管线中,将3D场景转换成2D图形的过程被分割成许多步骤。

  我们能看到的三维效果其实也是一系列的变化后的二维图片,这个过程就是opengl图像渲染管线要干的事。(Graphics Pipeline,大多译为管线,实际上指的是 一堆原始图形数据途经一个输送管道,期间经过各种变化处理,最终出现在屏幕的过程 )

  

  它们具有并行执行的特性,也就是说,就像自来水厂向你家输水一样,可以多条水管一起输过来。而当今大多数大脑的显卡上,都有成千上万的小处理核心(这个就像是那成千上万的“水管”),它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)
 

  其实随着技术的迭代跟新,已经由传统的顶点着色器和片段着色器,衍生了很多别的部分,如下图所示

管线流程简图:

   图形渲染管线可以被划分为两个主要部分:第一部分把 3D 坐标转换为 2D 坐标,第二部分把 2D 坐标转变为实际有颜色的像素

① 图形渲染管线的第一个部分是顶点着色器(Vertex Shader),它把一个单独的顶点作为输入。顶点着色器主要的目的是把 3D 坐标转为另一种 3D 坐标(后面会解释)。
图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入,并将所有的顶点装配成指定图元的形状,如三角形面片,三角形条带,直线,圆形等。
③ 图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它会通过新产生的顶点构造出新的图元来生成其他形状(但变形不变样)。
④ 几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。
⑤ 片段着色器(也称为片元着色器)的主要目的是计算一个像素的最终颜色

   图形渲染管线非常复杂,我这里写的还是冰山一角。然而,对于大多数场合,我们只需要配置顶点和片段着色器就行了。几何着色器是可选的,通常使用它默认的着色器就行了

  在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。

流程:

  

 

3.GLSL简介

  参考:https://learnopengl-cn.readthedocs.io/zh/latest/01%20Getting%20started/05%20Shaders/#glsl

  和其他编程语言一样,GLSL有数据类型可以来指定变量的种类。GLSL中包含C等其它语言大部分的默认基础数据类型:intfloatdoubleuintbool

  GLSL也有两种容器类型,它们会在这个教程中使用很多,分别是向量(Vector)和矩阵(Matrix),其中矩阵我们会在之后的教程里再讨论。 

  GLSL是为图形计算量身定制的,它包含一些针对向量和矩阵操作的有用特性。

  着色器的开头总是要声明版本(glsl的版本),接着是输入和输出变量uniformmain函数

  每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。

  典型着色器程序如下:

#version version_number

in type in_variable_name;
in type in_variable_name;

out type out_variable_name;

uniform type uniform_name;

int main()
{
  // 处理输入并进行一些图形操作
  ...
  // 输出处理过的结果到输出变量
  out_variable_name = weird_stuff_we_processed;
}

  当我们特别谈论到顶点着色器的时候,每个输入变量也叫顶点属性(Vertex Attribute)。我们能声明的顶点属性是有上限的,它一般由硬件来决定。OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,你可以查询GL_MAX_VERTEX_ATTRIBS来获取具体的上限:

GLint nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
View Code

  通常情况下它至少会返回16个,大部分情况下是够用了。

(0)指定GLSL版本

首行版本声明‌:必须指定 GLSL 版本和模式

 

#version 450 core  // 声明使用 OpenGL 4.5 Core Profile

 

 

 

(1)数据类型:

  GLSL中的向量是一个可以包含有1、2、3或者4个分量的容器,分量的类型可以是前面默认基础类型的任意一个。

  它们可以是下面的形式(n代表分量的数量):

类型含义
vecn 包含nfloat分量的默认向量
bvecn 包含n个bool分量的向量
ivecn 包含n个int分量的向量
uvecn 包含n个unsigned int分量的向量
dvecn 包含n个double分量的向量

  大多数时候我们使用vecn,因为float足够满足大多数要求了。一般就是:vec3即可。

  一个向量的分量可以通过vec.x这种方式获取,这里x是指这个向量的第一个分量。你可以分别使用.x.y.z.w来获取它们的第1、2、3、4个分量。GLSL也允许你对颜色使用rgba,或是对纹理坐标使用stpq访问相同的分量。

  向量重组:向量这一数据类型也允许一些有趣而灵活的分量选择方式,叫做重组(Swizzling)。重组允许这样的语法:

vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

  你可以使用上面4个字母任意组合来创建一个和原来向量一样长的(同类型)新向量,只要原来向量有那些分量即可;然而,你不允许在一个vec2向量中去获取.z元素。我们也可以把一个向量作为一个参数传给不同的向量构造函数,以减少需求参数的数量:

vec2 vect = vec2(0.5f, 0.7f);
vec4 result = vec4(vect, 0.0f, 0.0f);
vec4 otherResult = vec4(result.xyz, 1.0f);

  向量是一种灵活的数据类型,我们可以把它们用在各种输入和输出上。

(2)输入与输出:

  虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样这些管线【着色器】之间才能进行数据交流和传递

  GLSL定义了inout关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去

  但在顶点和片段着色器中会有点不同。  

【1】其中,对于顶点着色器:通过 layout(location=N) 指定顶点属性位置(如位置、法线、纹理坐标),通过 in 关键字声明‌

  顶点着色器应该接收的是一种特殊形式的输入,否则就会效率低下。顶点着色器的输入特殊在,它从顶点数据中直接接收输入。

  为了定义顶点数据该如何管理,我们使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。如:layout (location = 0)。顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。

注意:

  你也可以忽略layout (location = 0)标识符,通过在OpenGL代码中使用glGetAttribLocation查询属性位置值(Location),但是我更喜欢在着色器中设置它们,这样会更容易理解而且节省你(和OpenGL)的工作量

  顶点着色器输入举例子:

#version 330 core
layout (location = 0) in vec3 position; // position变量的属性位置值为0
out vec4 vertexColor; // 为片段着色器指定一个颜色输出
void main()
{
    gl_Position = vec4(position, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
    vertexColor = vec4(0.5f, 0.0f, 0.0f, 1.0f); // 把输出变量设置为暗红色
}
  • in:这是一个关键字,表示该变量是输入变量。在顶点着色器中,in 变量用于接收从 CPU 传递过来的顶点数据。
  • vec3:这是一个 OpenGL 着色器语言(GLSL)中的数据类型,表示三维向量。在这个例子中,vec3 用于表示顶点的三维位置坐标。
  • layout (location = 0):这是一个布局限定符(Layout Qualifier),用于指定顶点属性的位置。在 OpenGL 中,每个顶点属性(如:位置,法向量,纹理坐标等)都有一唯一的位置索引,这个索引用于在 CPU 和 GPU 之间传递顶点数据。这里将 position的位置索引设置为 0,意味着在后续的代码中,我们会使用这个索引来绑定顶点数据。 
  • void 表示这个函数没有返回值。main() 是顶点着色器的入口函数,就像 C 语言中的 main 函数一样,所有的顶点着色器代码都从这里开始执行。
  • gl_Position:这是一个特殊的内置变量,用于存储顶点的裁剪空间坐标。在顶点着色器中,必须为 gl_Position 赋值,否则 OpenGL 无法确定顶点的最终位置。

 

【2】片段着色器的输入输出。顶点着色器的 out 变量需与片段着色器的 in 变量名称、类型一致以实现数据传递‌。

  特殊性在于,它需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。

  核心任务:

 

举例子:

#version 330 core
in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)
out vec4 color; // 片段着色器输出的变量名可以任意命名,类型必须是vec4

void main()
{
    color = vertexColor;
}

  所以,如果我们打算从一个着色器向另一个着色器发送数据,我们必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。对于上述例子:你可以看到我们在顶点着色器中声明了一个vertexColor变量作为vec4输出,并在片段着色器中声明了一个类似的vertexColor。由于它们名字相同且类型相同,片段着色器中的vertexColor就和顶点着色器中的vertexColor链接了。由于我们在顶点着色器中将颜色设置为深红色,最终的片段也是深红色的。

 我们成功地从顶点着色器向片段着色器发送数据。让我们更上一层楼,看看能否从应用程序(CPU程序中设置)中直接给片段着色器(GPU程序)发送一个颜色!

 【3】attribute,precision修复符作用:

  attribute 是用于顶点着色器(Vertex Shader)的存储限定符(Storage Qualifier),仅用于顶点着色器(Fragment Shader 不能使用 attribute)。precision 是精度限定符(Precision Qualifier),用于控制变量的数值范围和精度。 

  

 
 

(3)Uniform

  Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。

  首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新

   我们可以在一个着色器脚本中添加uniform关键字,放在:类型和变量名前来声明一个GLSL的uniform。从此处开始我们就可以在着色器中使用新声明的uniform了。

#version 330 core
out vec4 color;

uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量

void main()
{
    color = ourColor;
} 

  我们在片段着色器中声明了一个uniform vec4ourColor,并把片段着色器的输出颜色设置为uniform值的内容。因为uniform是全局变量,我们可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。顶点着色器中不需要这个uniform,所以我们不用在那里定义它。

 注意:如果你声明了一个uniform却在GLSL代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误,记住这点

   由于这个uniform现在还是空的;我们还没有给它添加任何数据,所以下面我们就做这件事。。我们首先需要找到着色器中uniform属性的索引/位置值。当我们得到uniform的索引/位置值后,我们就可以更新它的值了。这次我们不去给像素传递单独一个颜色,而是让它随着时间改变颜色:

在CPU中的程序:
GLfloat timeValue = glfwGetTime();
GLfloat greenValue = (sin(timeValue) / 2) + 0.5;
GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");//查询uniform地址之前,不需要激活着色器程序
glUseProgram(shaderProgram);//使用着色器程序,将其激活
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

  解释:首先我们通过glfwGetTime()获取运行的秒数。然后我们使用sin函数让颜色在0.0到1.0之间改变,最后将结果储存到greenValue里。

   接着,我们用glGetUniformLocation查询uniform ourColor的位置值。我们为查询函数提供着色器程序和uniform的名字(这是我们希望获得的位置值的来源)。如果glGetUniformLocation返回-1就代表没有找到这个位置值。最后,我们可以通过glUniform4f函数设置uniform值。注意,查询uniform地址不要求你之前使用过着色器程序,但是更新一个unform之前你必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置unform的

 注意:因为OpenGL的GLSL语言本质核心是一个C库,所以它不支持类型重载。

因此:在函数参数不同的时候就要为其定义新的函数;glUniform是一个典型例子。这个函数有一个特定的后缀,标识设定的uniform的类型。可能的后缀有:

  

glUniform后缀含义
f 函数需要一个float作为它的值
i 函数需要一个int作为它的值
ui 函数需要一个unsigned int作为它的值
3f 函数需要3个float作为它的值
fv 函数需要一个float向量/数组作为它的值

每当你打算配置一个OpenGL的选项时就可以简单地根据这些规则选择适合你的数据类型的重载函数。在我们的例子里,我们希望分别设定uniform的4个float值,所以我们通过glUniform4f传递我们的数据(注意,我们也可以使用fv版本)。

 在CPU程序中修改:

CPU中修改,现在你知道如何设置uniform变量的值了,我们可以使用它们来渲染了。如果我们打算让颜色慢慢变化,我们就要在游戏循环的每一次迭代中(所以他会逐帧改变)更新这个uniform,否则三角形就不会改变颜色。
下面我们就计算greenValue然后每个渲染迭代都更新这个uniform:
while(!glfwWindowShouldClose(window)) { // 检测并调用事件 glfwPollEvents(); // 渲染 // 清空颜色缓冲 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); glClear(GL_COLOR_BUFFER_BIT); // 记得激活着色器 glUseProgram(shaderProgram); // 更新uniform颜色 GLfloat timeValue = glfwGetTime(); GLfloat greenValue = (sin(timeValue) / 2) + 0.5; GLint vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // 绘制三角形 glBindVertexArray(VAO); glDrawArrays(GL_TRIANGLES, 0, 3); glBindVertexArray(0); }

  这里的代码对之前代码是一次非常直接的修改。这次,我们在每次迭代绘制三角形前先更新uniform值。如果你正确更新了uniform,你会看到你的三角形逐渐由绿变黑再变回绿色。

思考:uniform对于设置一个在渲染迭代中会改变的属性是一个非常有用的工具,它也是一个在程序和着色器间数据交互的很好工具,但假如我们打算为每个顶点设置一个颜色的时候该怎么办?这种情况下,我们就不得不声明和顶点数目一样多的uniform了。在这一问题上更好的解决方案是在顶点属性中包含更多的数据,这是我们接下来要做的事情。

(4)更多属性配置

  我们了解了如何填充VBO、配置顶点属性指针以及如何把它们都储存到一个VAO里。这次,我们同样打算把颜色数据加进顶点数据中。我们将把颜色数据添加为3个float值至vertices数组。我们将把三角形的三个角分别指定为红色、绿色和蓝色:

GLfloat vertices[] = {
    // 位置              // 颜色
     0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // 右下
    -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // 左下
     0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // 顶部
};

  由于现在有更多的数据要发送到顶点着色器,我们有必要去调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。需要注意的是我们用layout标识符来把color属性的位置值设置为1:

顶点着色器程序:
#version 330 core
layout (location = 0) in vec3 position; // 位置变量的属性位置值为 0 
layout (location = 1) in vec3 color;    // 颜色变量的属性位置值为 1

out vec3 ourColor; // 向片段着色器输出一个颜色

void main()
{
    gl_Position = vec4(position, 1.0);
    ourColor = color; // 将ourColor设置为我们从顶点数据那里得到的输入颜色
}

修改片段着色器,这次不使用uniform进行变量传递,而是使用输入输出的方式进行传递:

片段着色器程序:
#version 330 core
in vec3 ourColor;
out vec4 color;

void main()
{
    color = vec4(ourColor, 1.0f);
}

因为我们添加了另一个顶点属性,并且更新了VBO的内存,我们就必须重新配置顶点属性指针

更新后的VBO内存中的数据现在看起来像这样:其中:stride(步幅),就是每个顶点属性占据的字节数量。位置的Position属性的Offset为0,Stride步幅为24, 颜色属性的offset偏置为12,stride步幅为24.

    知道了现在顶点属性的上述布局,我们就可以使用glVertexAttribPointer函数更新顶点格式,

// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);//表示定义顶点的属性数组0
glEnableVertexAttribArray(0);//表示启用顶点的属性数组0使用。
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3* sizeof(GLfloat)));//表示定义顶点的属性数组1
glEnableVertexAttribArray(1);//表示启用顶点的属性数组1使用。

   glVertexAttribPointer函数的前几个参数比较明了。这次我们配置属性位置值为1的顶点属性。颜色值有3个float那么大,我们不去标准化这些值。

  解释:由于我们现在有了两个顶点属性,我们不得不重新计算步长值。为获得数据队列中下一个属性值(比如位置向量的下个x分量)我们必须向右移动6个float,其中3个是位置值,另外3个是颜色值。这使我们的步长值为6乘以float的字节数(=24字节)。同样,这次我们必须指定一个偏移量。对于每个顶点来说,位置顶点属性在前,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是3 * sizeof(GLfloat),用字节来计算就是12字节。

  glEnableVertexAttribArray(0);表示使能索引为0的顶点属性数组。

 

函数原型:

(1)定义顶点属性数组 glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,const GLvoid * pointer)

 

void glVertexAttribPointer(    GLuint    index,
                    GLint    size,
                  GLenum    type,
                  GLboolean    normalized,
                  GLsizei    stride,
                  const GLvoid *    pointer);        

 

  index:指定要修改的顶点属性的索引值

  size:指定每个顶点属性的组件(components)数量,必须为1、2、3或者4。初始值为4,例如:顶点含有三个维度,则x,y,z,组件数量为3.

  type: 组件元素对应的数据类型,必须用opengl定义的符号,如:GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT,GL_UNSIGNED_SHORT, GL_FIXED, 和 GL_FLOAT,初始值为GL_FLOAT。

  normalized:指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE)。一般不需要归一化,直接按照值的大小接收。

  stride: 指定连续【顶点属性】之间的偏移量,称为“步幅stride”,例如:可理解为两个位置属性值之间的间隔。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0。

  pointer:指定第一个组件(如:1个顶点的位置属性的第一个组件x分量)在数组的【第一个顶点属性】中的偏移量。该数组与GL_ARRAY_BUFFER绑定,储存于缓冲区中。初始值为0;

 

注意理解:

  • 一般将所有顶点的属性都按照顶点的顺序定义在一个一维数组中,一个顶点的属性数量一般最大支持16个,可以查询获得。

  • 所有的顶点的属性都按照相同的排列方式进行一维排列。

  • 顶点的每个属性都需要用“glVertexAttribPointer()”函数来定义:顶点不同的属性的获取方式。其中,index指明顶点属性的第几个属性,size是该属性的组件数量,type是每个组件的数据类型,stide指定第一个顶点的当前属性值到下一个顶点的当前属性值之间的字节步长;normalized一般为false,属性值是多少就读取多少,如果为GL_True,则如果是float浮点数,则整数类型的值会倍映射到区间[-1,1]有符号整数,或者[0,1]无符号整数,而进行归一化处理。pointer是定义顶点的不同属性中,当前属性的第一个组件在【顶点属性排列中】的偏移量offset。

  • glVertexAttribPointer一般在CPU程序(客户端)实现。

  • 在调用glVertexAttribPointer之前,需要绑定:VBO,并通过glEnableVertexAttribArray启用对应的属性数组。

  • 此外,最有一个参数:pointer,数据类型是:void*,数据指针, 这个值受到VBO的影响;在不使用VBO的情况下,就是一个指针,指向的是需要上传到顶点数据指针,项目中通常在不使用VBO的情况下,绘制之前,执行glBindBuffer(GL_ARRAY_BUFFER, 0),否则会导致数组顶点无效,界面无法显示;2:使用VBO的情况下,先要执行glBindBuffer(GL_ARRAY_BUFFER, 1),如果一个名称非零的缓冲对象被绑定至GL_ARRAY_BUFFER目标(见glBindBuffer)且此时一个定点属性数组被指定了,那么pointer被当做该缓冲对象数据存储区的字节偏移量。并且,缓冲对象绑定(GL_ARRAY_BUFFER_BINDING)会被存为索引为index的顶点属性数组客户端状态;此时指针指向的就不是具体的数据了。因为数据已经缓存在缓冲区了。这里的指针表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头,所以这里是0。

 

举例子:

    1
//不使用VBO机制,则glBindBuffer(GL_ARRAT_BUFFER,0),则表示opengl将不再使用任何 GL_ARRAY_BUFFER类别的“缓冲区对象”
virtual void Draw(const EtFrame *frame) { glClearColor(0.5, 0.5, 0.5, 0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); float vertices[] = {//顶点位置+颜色属性数组,GPU端(客户端)的数据源 // 位置 // 颜色 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, // 右下 -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, // 左下 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 顶部 }; //GLuint VBO; //glGenBuffers(1, &VBO); //glBindBuffer(GL_ARRAY_BUFFER, VBO); //glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); glBindBuffer(GL_ARRAY_BUFFER, 0);//这个解绑不能少了,否则绘制不出来。注意,此处表示:不使用VBO【顶点缓冲区对象】,直接使用CPU端的顶点数组进行绘制。 EsProgramParticle *prog = EsGetProgramParticle(); if (NULL == prog) return; glUseProgram(prog->program); glEnableVertexAttribArray(prog->a_xyz);//启用【激活】顶点位置索引属性 glEnableVertexAttribArray(prog->a_rgba);//启用【激活】顶点颜色索引属性 glVertexAttribPointer(prog->a_xyz, 3, GL_FLOAT, GL_FALSE, 7 * sizeof(GLfloat), vertices);//定义属性为a_xyz【属性索引】的“顶点位置属性” glVertexAttribPointer(prog->a_rgba, 4, GL_FLOAT, GL_FALSE, 7 * sizeof(GLfloat), (void*)(vertices + 3));//定义属性为a_rgba【属性索引】的“顶点颜色属性” //glVertexAttribPointer(prog->a_xyz, 3, GL_FLOAT, GL_FALSE, 7 * sizeof(GLfloat), (void *)0);// (void *)0); //glVertexAttribPointer(prog->a_rgba, 4, GL_FLOAT, GL_FALSE, 7 * sizeof(GLfloat), (void*)(3 * sizeof(float))); glDrawArrays(GL_TRIANGLES, 0, 3); } };
//使用VBO机制示例:
virtual
void Draw(const EtFrame *frame) { glClearColor(0.5, 0.5, 0.5, 0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); float vertices[] = { // 位置 // 颜色 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, // 右下 -0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, // 左下 0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, // 顶部 };
GLuint VBO; glGenBuffers(
1, &VBO); //使用:缓存对象机制,向GPU申请1个顶点缓存对象,并后去缓存对象名称ID。 glBindBuffer(GL_ARRAY_BUFFER, VBO);//将此缓冲区绑定到:GL_ARRAY_BUFFER类别“缓冲对象点”上 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);// 在GPU中分配一块指定大小的GL_ARRAY_BUFFER类别的“缓冲区”,并用vertices进行初始化 //glBindBuffer(GL_ARRAY_BUFFER, 0);//这个不能解绑,否则也绘制不出来。 EsProgramParticle *prog = EsGetProgramParticle(); if (NULL == prog) return; glUseProgram(prog->program); glEnableVertexAttribArray(prog->a_xyz); glEnableVertexAttribArray(prog->a_rgba); //glVertexAttribPointer(prog->a_xyz, 3, GL_FLOAT, GL_FALSE, 7 * sizeof(GLfloat), vertices); //glVertexAttribPointer(prog->a_rgba, 4, GL_FLOAT, GL_FALSE, 7 * sizeof(GLfloat), (void*)(vertices + 3)); glVertexAttribPointer(prog->a_xyz, 3, GL_FLOAT, GL_FALSE, 7 * sizeof(GLfloat), (void *)0);// (void *)0); glVertexAttribPointer(prog->a_rgba, 4, GL_FLOAT, GL_FALSE, 7 * sizeof(GLfloat), (void*)(3 * sizeof(float))); glDrawArrays(GL_TRIANGLES, 0, 3); } };

 

 

(*)缓冲区概念:

  •   缓冲区对象,允许应用程序迅速方便地将数据从一个渲染管线移动到另一个渲染管线,以及从一个对象绑定到另一个对象,不仅可以把数据移动到合适的位置,还可以在无需CPU介入的情况下完成这项工作。
  •        缓冲区保存在GPU内存中【可以被称为“GPU缓冲区对象”】,它们提供高速和高效的访问,但在GPU中更新数据常常需要重新加载整个对象,在系统内存和GPU内存之间来回移动数据可能是一个缓慢的过程。
  •   缓冲区有很多不同的用途,它们能够保存顶点数据,纹理数据,可以在渲染管线的GPU中任意访问。
  •        创建缓冲区:只需要调用glGenBuffers函数,来为我们所需任何数量的新缓冲区创建名称,实际缓冲区对象将在第一次使用时创建
  •   绑定缓冲区:获取缓冲区名称后ID后,就可以用这个缓冲区名称ID来绑定缓冲区。注意:不同“缓冲区类型”,可以看作“绑定点”类别,每种绑定点只能绑定一个“缓冲区名称ID”。

(2)【向GPU申请缓冲区名称,获取“缓冲区名称ID”】glGenBuffers (GLsizei n,GLuint * buffers);

  作用:函数用来生成缓冲区对象的名称

  函数参数:第一个参数n是要生成的缓冲对象的数量,第二个参数buffers是要输入用来“存储缓冲对象名称”的数组【接收GPU生成的缓冲区对象名称,就是GLuint类型】

  个人理解如下,可以声明一个GLuint变量,然后使用glGenBuffers后,它就会把缓冲对象保存在vbo里,当然也可以声明一个数组类型,那么创建的3个缓冲对象的名称会依次保存在数组里。

举个例子:

GLuint vbo;//用来接收GPU端创建的“存储缓冲对象”名称,就是ID
glGenBuffers(1,&vbo);//创建1个,用地址
GLuint vbo[3];//创建3个,要用到数组
glGenBuffers(3,vbo);

  

注意:这里我用的是VBO做的示范,解释一下,glGenBuffers()函数仅仅是生成一个缓冲对象的名称,这个【缓冲对象(仅仅是个ID)】并不具备任何意义,它仅仅是个缓冲对象,还不是一个顶点数组缓冲,它类似于C语言中的一个指针变量,我们可以分配内存对象并且用它的名称来引用这个内存对象。OpenGL有很多缓冲对象类型,那么这个缓冲对象到底是什么类型,就要用到下面的glBindBuffer()函数了。
 此外:glCreateBuffers和glGenBuffers一样,但是前者在opengl4.5开始支持,而后者支持所有版本。

(3)【绑定缓冲区】glBindBuffer(GLenum target, GLuint buffer);

  作用:glBindBuffer函数是用于绑定指定的缓冲区对象到当前的,以便在后续的操作中使用。这个函数的作用类似于设置一个“当前工作的缓冲区”,所有对缓冲区的操作都会影响到这个“当前工作的缓冲区”。

  函数原型:void glBindBuffer(GLenum target, GLuint buffer);

  函数参数:

  第一个参数:target参数指定了缓冲区对象的类型,它决定了缓冲区将如何被使用。例如,GL_ARRAY_BUFFER用于顶点数组数据,而GL_ELEMENT_ARRAY_BUFFER用于索引缓冲区,这些类型决定了数据的使用方式和访问模式。

  第二个参数:buffer参数是缓冲区对象的名称,这个名称是通过glGenBuffers函数生成的【缓冲对象(仅仅是个ID)】。如果把target绑定到一个已经创建好的缓冲对象,那么这个缓冲对象将为当前target的激活对象;但是,buffer为0时,OpenGL将不再使用当前target【类被】的任何缓冲区对象

   在OpenGL红宝书中给出了一个恰当的比喻:绑定对象的过程就像设置铁路的道岔开关,每一个缓冲类型中的各个对象就像不同的轨道一样,我们将开关设置为其中一个状态,那么之后的列车都会驶入这条轨道,注意:

切记:官方文档指出,GL_INVALID_VALUE is generated if buffer is not a name previously returned form a call to glGenBuffers。换句话说,这个名称虽然是GLuint类型的,但是你万万不能直接指定个常量比如说0。如果你这样做,就会出现GL_INVALID_VALUE的错误。

  注意:

  • OpenGL允许我们同时绑定多个缓冲类型,只要这些缓冲类型是不同的,换句话说,同一时间,不能绑定两个相同类型的缓冲对象。也可以理解为对于一个类型来说,同一时间只能“激活”一个类型,否则就会发生“矛盾”。
  • 为什么呢。首先要理解绑定缓冲类型后,所有该缓冲类型的函数调用都要用来配置该目标缓冲类型,比如顶点缓冲类型GL_ARRAY_BUFFER,glBufferData是通过指定目标缓冲类型来进行数据传输的,而每一个目标缓冲类型再使用前要提前绑定一个缓冲对象,从而赋予这个缓冲对象一个类型的意义,如果绑定了两个相同类型的目标缓冲,数据的配置肯定就会出错。(可以这样想一下,我要把数据存入顶点缓冲区,但是顶点缓冲区可以有很多缓冲对象,我需要传入哪个呢,于是我就要提前绑定一个,之后,我只要向顶点缓冲区内传入数据,这个数据就会自动进入被绑定的那个对象里面)。就类似于:火车岔道,对于相同类型的缓冲区,每次只能有1个缓冲区作为当前“”激活缓冲区”。

  • 之后调用glBufferData()传输所需数据,其中第一个参数就是要制定缓冲类型,根据这个类型锁定当前唯一的目标缓冲
  • 以下是:不同的缓冲区类型,Opengl中定义的类别,还被作为“缓冲区对象绑定点”:

    常用的是GL_ARRAY_BUFFER(用作位置,颜色,纹理坐标等顶点属性,或其他自定义属性)和GL_ELEMENT_ARRAY_BUFFER(用于索引数组缓冲区保存)两种。

 

glBindBuffer(GL_ARRAY_BUFFER, VBO);  //VBO变成了一个顶点缓冲类型,将:VBO作为当前激活的“内存缓冲区”
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

(4)【填充缓冲区】glBufferData(GLenum target, GLsizeiptr size, const GLvoid * data, GLenum usage);

  作用:glBufferData 是 OpenGL 中用于分配并初始化【缓冲区对象】对应的数据存储的函数。它将用户定义的数据复制到【当前绑定的缓冲区对象】中。

   简单理解:用glBufferData函数向GPU中申请【分配】特定类别的缓冲区,并将CPU中的【顶点属性数组值】上传到当前绑定的特定类别的“缓冲区”中,完成【初始化】。

 

  参数说明:

  target: 指定目标缓冲区对象【缓冲区类别,缓冲区类型,缓冲区对象绑定点】。符号常量必须为 GL_ARRAY_BUFFER 或 GL_ELEMENT_ARRAY_BUFFER

  size: 指定缓冲区对象的新数据存储的大小(以字节为单位)。

  data:需要上传的【CPU中】数据源指针,将data指向的数据复制到“GPU数据内存缓冲区”中。如果想要分配一个特定大小的缓冲区却不需要立即对它进行填充,那么这个指针也可以说NULL

  usage: 指定数据存储的预期使用模式。符号常量必须为 GL_STREAM_DRAWGL_STATIC_DRAW 或 GL_DYNAMIC_DRAW

  说明如下:

  • GL_STATIC_DRAW:数据不会或几乎不会改变
  • GL_DYNAMIC_DRAW:数据会被改变很多
  • GL_STREAM_DRAW :数据每次绘制时都会改变

  使用方式如下:

   在不确定缓冲区用途时,对于通常的缓冲区使用方式或条件来说,使用GL_DYNAMIC_DRAW是一个比较安全的值,可以重复调用glBufferData对缓冲区进行填充,还可以改变使用方式,但是重复调用以后,缓冲区中原来的数据会被删除,可以使用函数glBufferSubData对已经存在的缓冲区中的一部分进行更新,而不会导致缓冲区其他部分的内容变为无效。

void glBufferSubData(GLenum target,intptr offset,sizeiptr size,const void*data);
  参数与glBufferData函数类似,其中不同的 offset参数 指定从开头起 计算偏移到哪个位置开始更新数据,因为内存已经被分配,所以不能改变缓冲使用方式;
  注意事项:
  • 在调用:在调用glBufferData之前,必须将需要使用的特定类型的缓冲区进行绑定,作用类似:激活特定类型的特定索引的缓冲区。表示:后续的所有缓冲区的操作都是针对当前“激活的缓冲区”。因此:对glBufferData使用的缓冲区【缓冲区名称ID】目标与绑定的【缓冲区名称ID】目标相同;

 

 (5)【删除缓冲区】

  当使用完一个缓冲区后,这个缓冲区需要进行清除,通过调用函数glDeleteBuffer来删除它;

 

 

 

  运行程序你应该会看到如下结果:

 

  

4. 着色器类型

(1)顶点着色器(Vertex Shader)

  • 功能:顶点着色器是处理顶点数据的第一个阶段。它接收顶点的原始数据(如顶点坐标、法向量、颜色等),并对这些数据进行变换和处理,例如将顶点坐标从模型空间转换到裁剪空间。

  核心任务:顶点着色器(Vertex Shader):是图形渲染管线的第一个可编程阶段,负责处理每个顶点的属性,它的核心任务包括

  • 顶点坐标变换:将顶点的局部坐标(模型空间)通过模型-视图-投影矩阵转换为裁剪空间坐标(NDC坐标)
  • 属性传递:计算顶点的法线、颜色、纹理坐标等属性,并将这些数据传递给后续阶段(如片段着色器)
  • 基础动画与特效:实现骨骼动画、粒子系统动态效果等。

顶点着色器的代码文件,用于在GPU中执行的:

#version 330 core
layout (location = 0) in vec3 aPos;  // 输入的顶点位置
layout (location = 1) in vec3 aColor; // 输入的顶点颜色
out vec3 ourColor;  // 输出的颜色,传递给片段着色器
void main()
{
    gl_Position = vec4(aPos, 1.0); // 将顶点位置转换为齐次坐标
    ourColor = aColor; // 传递颜色数据
}

  这段顶点着色器代码的核心功能是将输入的三维顶点位置转换为四维齐次坐标,并将其作为裁剪空间坐标输出。

  • #version 330 core 表示:这是一个预处理指令,用于指定使用的 OpenGL 着色器语言(GLSL)版本。这里指定使用的是 GLSL 3.30 核心版本。核心版本意味着只使用 OpenGL 核心功能,不包含一些已被弃用的特性
  • in:这是一个关键字,表示该变量是输入变量。在顶点着色器中,in 变量用于接收从 CPU 传递过来的顶点数据。
  • vec3:这是一个 OpenGL 着色器语言(GLSL)中的数据类型,表示三维向量。在这个例子中,vec3 用于表示顶点的三维位置坐标。
  • layout (location = 0):这是一个布局限定符(Layout Qualifier),用于指定顶点属性的位置。在 OpenGL 中,每个顶点属性(如:位置,法向量,纹理坐标等)都有一唯一的位置索引,这个索引用于在 CPU 和 GPU 之间传递顶点数据。这里将 position的位置索引设置为 0,意味着在后续的代码中,我们会使用这个索引来绑定顶点数据。 
  • void 表示这个函数没有返回值。main() 是顶点着色器的入口函数,就像 C 语言中的 main 函数一样,所有的顶点着色器代码都从这里开始执行。
  • gl_Position:这是一个特殊的内置变量,用于存储顶点的裁剪空间坐标。在顶点着色器中,必须为 gl_Position 赋值,否则 OpenGL 无法确定顶点的最终位置。

 

  注意:这里gl_position的是一个vec4类型的内置变量。 vec4 是一个四维向量数据类型,用于表示齐次坐标。在 OpenGL 中,顶点的位置通常使用齐次坐标来表示,齐次坐标的前三个分量表示三维空间中的位置,第四个分量通常设置为 1.0。这里将 aPos(三维向量)转换为 vec4 类型,第四个分量设置为 1.0,然后将其赋值给 gl_Position。

 

(2)片段着色器:Fragment Shader

   功能:段着色器负责计算每个像素的最终颜色。它接收顶点着色器传递过来的数据(如颜色、纹理坐标等),并根据这些数据计算出每个像素的颜色值。

 片段着色器是渲染管线的最后阶段,负责计算每个像素(片段)的最终颜色,其核心任务包括

  • 颜色计算:基于光照模型(如Phong、Lambert)计算像素颜色
  • 纹理映射:通过纹理坐标采样贴图,为像素添加细节
  • 特效处理:实现阴影、模糊、透明等视觉效果

 

 片段着色器程序,用于GPU中运行

#version 330 core
out vec4 FragColor;  // 输出像素颜色
uniform sampler2D uTexture;  // 纹理采样器
varying vec2 vTexCoord;       // 从顶点着色器传递的纹理坐标
void main() {
    FragColor = texture(uTexture, vTexCoord);  // 纹理采样结果作为颜色
}

  out vec4 FragColor; 其中:

  •   out:这是一个关键字,表示该变量是输出变量。在片段着色器中,out 变量用于将计算得到的像素颜色输出到后续的渲染阶段。
  •   vec4:这是一个 OpenGL 着色器语言(GLSL)中的数据类型,表示四维向量。在这个例子中,vec4 用于表示像素的颜色,四个分量分别代表红色(R)、绿色(G)、蓝色(B)和透明度(A),取值范围通常在 [0.0, 1.0] 之间。
  •   FragColor:这是变量名,用于存储最终输出的像素颜色。

  uniform sampler2D uTexture;

  • uniform:这是一个关键字,表示该变量是全局统一变量。在 OpenGL 中,uniform 变量的值在一次渲染过程中保持不变,可以在 CPU 端设置,然后传递给 GPU 端的着色器使用
  • sampler2D:这是一个特殊的数据类型,用于表示二维纹理采样器。在 OpenGL 中,纹理采样器用于从纹理中采样颜色
  • uTexture:这是变量名,用于表示纹理采样器。在后续的代码中,我们将使用这个采样器从纹理中采样颜色。

   varying vec2 vTexCoord;

 

  • varying:这是一个关键字,用于在顶点着色器和片段着色器之间传递数据。在顶点着色器中定义的 varying 变量会被插值计算,然后传递给片段着色器
  • vec2:这是一个二维向量数据类型,用于表示纹理坐标。纹理坐标通常是二维的,范围在 [0.0, 1.0] 之间,其中 (0.0, 0.0) 表示纹理的左下角,(1.0, 1.0) 表示纹理的右上角。
  • vTexCoord:这是变量名,用于存储从顶点着色器传递过来的纹理坐标。

  

 

 

 

(3)着色器的属性:

  理解 OpenGL 着色器语言(GLSL)中的 layoutinout 等属性是非常重要的,这些属性用于定义着色器之间的数据传递和属性的存储位置。

 【1】layout属性:

  layout 属性主要用于指定着色器输入输出变量的存储布局,比如在顶点着色器中指定顶点属性的位置,或者在片段着色器中指定颜色输出的位置等。通过 layout 属性,我们可以精确地控制数据在内存中的存储和访问方式,方便 OpenGL 正确地读取和处理数据。

#version 330 core
// 顶点着色器中,使用 layout 指定顶点位置属性的位置为 0
layout (location = 0) in vec3 aPos;
// 使用 layout 指定顶点颜色属性的位置为 1
layout (location = 1) in vec3 aColor;
out vec3 ourColor;

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor;
}

  在上述顶点着色器代码中,layout (location = 0) 表明 aPos 这个顶点位置属性在顶点数据中的索引为 0,layout (location = 1) 则表示 aColor 顶点颜色属性的索引为 1。

  在 C++ 代码中,我们可以根据这些索引来设置【定义】顶点属性指针,示例如下:

// 设置顶点位置属性指针
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)0);//定义属性0为顶点位置属性的属性指针,用于“顶点着色器”的数据交互
// 设置顶点颜色属性指针
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (GLvoid*)(3 * sizeof(GLfloat)));//定义顶点属性1为顶点颜色属性的属性指针,用于顶点颜色着色器的数据交互。

 【2】int 属性

   in 属性用于声明着色器的输入变量。在不同类型的着色器中,in 变量的来源不同。例如,在顶点着色器中,in 变量通常是从 CPU 传递过来的顶点属性数据;在片段着色器中,in 变量一般是从顶点着色器经过光栅化插值后传递过来的数据。

  例如,上例子中,在顶点着色器中,aPos 和 aColor 是从 CPU 传递过来的顶点属性,使用 in 关键字声明。而 ourColor是输出变量,会传递给片段着色器。在片段着色器中,ourColor 作为输入变量接收来自顶点着色器的数据,然后将其用于计算最终的像素颜色。

【3】out 属性

  out 属性用于声明着色器的输出变量。这些变量会传递给下一个阶段的着色器进行处理。例如,顶点着色器的输出变量会经过光栅化插值后传递给片段着色器。

   例如:上例子中,在顶点着色器中,ourColor 使用 out 关键字声明,它会将顶点的颜色信息传递给片段着色器。在片段着色器中,FragColor 使用 out 关键字声明,它表示最终的像素颜色,会被写入帧缓冲区。

 

 

 

5. NDC(归一化设备坐标, NDC, Normailized Device Coordinates)

    NDC(Normalized Device Coordinates 归一化设备坐标):是图形渲染管线中的一个标准化坐标系统,用于将三维模型从不同空间统一映射到屏幕空间前的中间步骤.

  其核心特点包括:

  • NDC可视作一个中心在原点、边长为2的立方体,所有可见物体必须位于此空间内

  • OpenGL中NDC为左手坐标系​z轴正方向朝向屏幕内部

  二维NDC如下所示:

  

NDC  三维坐标系示意图,其中,Z轴为屏幕内部。

   假定三维坐标:

三个顶点的坐标数据,三维坐标分别是(-0.5f, -0.5f, 0.0f)、(0.5f, -0.5f, 0.0f)和( 0.0f, 0.5f, 0.0f),因为是二维图形,所以z zz轴都为0即可,具体的顶点数据如下:

// 顶点数据
float vertices[] = {
    -0.5f, -0.5f, 0.0f,
    0.5f, -0.5f, 0.0f,
    0.0f,  0.5f, 0.0f
};

  

因为绘制的是NDC坐标,所以只有[ − 1 , 1 ] [-1,1][−1,1]之间的数据有效,按照比例变化后,实际上的顶点位置如下:
 

 

【1申请ID】为VAO和VBO申请ID
    glGenVertexArrays(1, &VAO_ID);
    glGenBuffers(1, &VBO_ID);

【2绑定】绑定VAO、VBO对象
    glBindVertexArray(VAO_ID);
    glBindBuffer(GL_ARRAY_BUFFER, VBO_ID);

【3申请分配VBO并初始化VBO】操作VBO,为当前绑定的缓冲区对象创建一个新的数据存储,这一步实际上是为了在GPU上创建对应的存储区域,并将内存中的数据发送过去
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

【5.定义顶点属性指针】告知GPU如何去解析缓冲区里的属性值,顶点属性指针保存在与之绑定的VAO中。
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);  // 第一个属性,所以不需要偏移
6. 使能顶点属性索引】
     glEnableVertexAttribArray(0);

7. 解绑VBO,和VAO】
     glBindBuffer(GL_ARRAY_BUFFER, 0);//解绑当前绑定的VBO
     glBindVertexArray(0);//解放当前绑定的VAO

其他VAO和VBO的配对操作...
     ......
        

以上阶段:都需要在initialize()初始化函数中完成。
因为OpenGL是一个状态机,我们需要对每一帧进行绑定,并且因为之前初始化后解绑了,如果不绑定就没有内容了:

绘制阶段,如果需要绘制上述VBO_ID和VAO定义的内容,则重新绑定VAO即可。
     glBindVertexArray(VAO);
利用VAO和VBO绘制三角:
     glDrawArrays(GL_TRIANGLES, 0, 3);
再次解绑。
     glBindBuffer(GL_ARRAY_BUFFER, 0);//解绑当前绑定的VBO
     glBindVertexArray(0);//解放当前绑定的VAO
以上绘制出来的没有颜色,如果需要添加颜色,还需要在着色器中添加着色器程序。 

 

 

 

 

6. 着色器编程一般步骤

 

(1) 一般来讲,引入opengl相关的库

#include <iostream>
using namespace std;
#define GLEW_STATIC    
#include <GL/glew.h>    
#include <GLFW/glfw3.h> 

(2)定义渲染的顶点:

  我们要渲染一个三角形,一共要指定三个顶点,每个顶点都有一个 3D 位置:

GLfloat vertices_1[] = 
{
    0.0f, 0.5f, 0.0f,        // 上顶点
    -0.5f, -0.5f, 0.0f,        // 左顶点
    0.5f, -0.5f, 0.0f,        // 右顶点
};

  

定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。

 (3)编写顶点着色器
  需要编写顶点着色器脚本程序,用GLSL语言, 然后编译,这样就在程序中使用。
  此处,直接在C++代码里直接写,为了便于演示。不再作为单独的着色器文件。
// 顶点着色器
const GLchar* vertexCode_1 = "#version 330 core\n"        // 3.30版本(版本申明)
"layout(location = 0) in vec3 position_1;\n"            // 三个浮点数 vector 向量表示位置。position是变量,并储存了这三个向量
"void main()\n"
"{\n"
"gl_Position = vec4(position_1, 1.0f);\n"                // 核心函数(位置信息赋值)
"}\n";

  为了设置顶点着色器的输出,我们必须把位置数据赋值给预定义的 gl_Position变量 ,它在幕后是 vec4 类型的。然后,声明输入变量可以使用 in 关键字。由于我们的输入是一个 3 分量的向量,我们必须把它转换为 4 分量的。我们可以把 vec3 的数据作为 vec4 构造器的参数,同时把 “透视值分量” 设置为1.0f(后面课程会解释为什么)来完成这一任务。

(4)配置CPU端顶盖属性值相关操作

  有了顶点着色器这个 “程序”,以及定义了顶点着色器的输入以及属性类型,我们需要在CPU程序中完成数据交互:使用VBO和不使用VBO两种方式。

  不管哪种方式,都需要在CPU程序中指定顶点属性的定义,与顶点着色器程序一一对应。

  用glVertexAttribPointer函数告诉GPU如何解析顶点数据(或者称为:顶点属性数据)。

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(GLfloat), (GLvoid*)0); //定义顶点属性0,为顶点位置属性的指针。
    glEnableVertexAttribArray(0);            // 启用顶点属性(注:顶点属性默认是禁用的)。注意,启动定义的顶点属性值。

  【1】使用VBO(顶点缓冲对象)方式,还需要配合VAO(顶点数组对象)一起使用。

  使用VBO的优势:我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。

  我们通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这含有这三个顶点的内存。

  【第一】顶点缓冲对象是我们在 OpenGL 出现的一个重要的 OpenGL 对象,可以做很多事情。和 OpenGL 中的其它对象一样,这个缓冲对象也有一个独一无二的 ID ,所以我们可以使用 glGenBuffers 函数来向GPU申请一个顶点缓冲对象ID(可以根据需要VBO的数量,申请多个VBO的ID)。

    GLuint VBO_ID;                  // 顶点缓存对象
    glGenBuffers(1, &VBO_ID);        // VBO 主要负责传输数据   绑定 VBO

  【第二】由于OpenGL 有很多缓冲对象类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。我们可以使用 glBindBuffer 函数把新创建的顶点缓冲对象绑定到 GL_ARRAY_BUFFER 类型目标上,本质上激活当前GL_ARRAY_BUFFER 类型的顶点缓冲对象,表示后续所有的VBO操作都是针对GL_ARRAY_BUFFER 类型的

glBindBuffer(GL_ARRAY_BUFFER, VBO_ID);    

  【第三】从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的特定缓冲对象ID对应的顶点缓冲对象(VBO)。

  (本质上,可以类比于火车的岔道转接闸门, 将GL_ARRAY_BUFFER类型的多个VBO名称比喻为多路火车道路,glBindBuffer作用就是将后续的操作应用在“哪个VBO的ID”上,也就是激活该名称ID对应的VBO,如下图。

 。然后我们可以调用 glBufferData 函数,它会把之前定义的顶点数据复制到缓冲的内存中:

  glBufferData(GL_ARRAY_BUFFER, sizeof(vertices_1), vertices_1, GL_STATIC_DRAW);

  则表示,当前是向变量名称VBO_ID中分配vertices_1大小的GPU内存区间,并用其数据上传初始化该内存区域。 GL_STATIC_DRAW表示后续该内存就不再变化。

显卡管理数据的形式。它有三种形式:
    GL_STATIC_DRAW :数据不会或几乎不会改变。
    GL_DYNAMIC_DRAW:数据会被改变很多。
    GL_STREAM_DRAW :数据每次绘制时都会改变。

  因为三角形的位置数据不会改变,每次渲染调用时都保持原样,所以它的使用类型最好是GL_STATIC_DRAW。

  如果glBindBuffer(GL_ARRAY_BUFFER, VBO名称ID2),则表示当前激活的是VBO名称2对应的“顶点缓冲对象” 。   

(5)设置顶点数组对象(VAO) (也称顶点阵列对象)

  VAO ( Vertex Array Object )是OpenGL用来处理顶点数据的一个缓冲区对象,它不能单独使用,都是结合VBO来一起使用的。

   顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个 VAO 中。

  这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的 VAO 就行了。这使得在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的 VAO 就行了。刚刚设置的所有状态都将存储在 VAO 中。

  创建一个 VAO 和创建一个 VBO 很类似:

     GLuint VAO_ID;                       // 顶点阵列对象
    glGenVertexArrays(1, &VAO_ID);        // VAO 对应的一种 关联信息(顶点着色器和位置相映射)  绑定 VAO

   要想使用 VAO_ID,要做的只是使用 glBindVertexArray 绑定 VAO_ID 。由于当前已经激活对应的VBO_ID对应的顶点缓冲区VBO,因此执行 glBindVertexArray(VAO_ID),本质上,就是已经建立了VAO和VBO的绑定关系。

  如果需要解放当前激活的VBO和与之绑定的VAO,则直接调用:glBindVertexArray(0),即可。从而解绑:VBO和 VAO (释放)供之后使用,直接绑定0即可表示释放解放。

    glBindVertexArray(VAO_ID);        // 绑定 VAO_ID 和 VBO_ID
    //glBindVertexArray(0);        // 解绑定(VAO 和 VBO)的代码

  一般当你打算绘制多个物体时,你首先要 生成/配置 所有的 VAO(和必须的 VBO),然后储存它们供后面使用。

  调用流程就是:当我们打算绘制物体的时候就拿出相应的 VAO,绑定它,绘制完物体后,再解绑 VAO 即可。

   这里提一下 VAO 和 VBO 之间的关系:

我们用顶点着色器处理顶点数据的时候,着色器程序是没有输入的。它调用的时候需要的数据怎么获得呢,就需要 VAO 来帮忙。但是 VAO 只是帮助如何解读,不是数据,数据的导入和索引是VBO负责。所以一条管线的着色器必须有一个 VAO 来解释对应的 VBO 中的数据。比如我们课上,position 就是需要的输入变量,但是调用着色器程序时没有写输入参数。这个变量就需要从 VAO 寻找解读方式,然后用这个解读方式在对应的 VBO 中解读数据。因为此刻 VBO 对应的是在显存里的数据,都是二进制代码,如何解读成有意义的数据格式就是 VAO 来负责并关联到 position 上 

 

【6】撰写顶点着色器和片段着色器程序片段

顶点着色器:
const char *vertexShaderSource = "#version 330 core\n"
                                 "layout (location = 0) in vec3 aPos;\n"
                                 "void main()\n"
                                 "{\n"
                                 "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
                                 "}\0";


片段着色器:
const char *vertexShaderSource = "#version 330 core\n"
                                 "layout (location = 0) in vec3 aPos;\n"
                                 "void main()\n"
                                 "{\n"
                                 "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
                                 "}\0";

【7】创建顶点着色器和片段着色器,并与着色器源文件关联起来。

类似于从GPU获取着色器ID。

创建Vertex Shader 顶点着色器:
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); 
创建片段着色器,
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER); 

绑定着色器与源程序绑定在一起。
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);  // 绑定至着色器原码
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);

【8】编译着色器,

分别编译上述关联好的着色器
glCompileShader(vertexShader);  // 编译着色器
glCompileShader(fragmentShader);

查看编译结果,判断是否编译成功,失败则打印出错误日志:
 /* 因为可能出错,所以进行错误检查,也就是判断是否成功编译 */
    int success;  // 是否成功的标志
    char infolog[512];  // 错误日志(信息)
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if(!success)
    {
        glGetShaderInfoLog(vertexShader, 512, NULL, infolog);
        qDebug() << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infolog;
    }

    //片段着色器
      glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infolog);
        qDebug() << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infolog;
    }

【9】链接着色器:

  编译好两个着色器后,我们需要先创建一个连接程序,然后把着色器链接到程序上,此分别加入顶点着色器和片段着色器,最后链接起来:

    /* 链接顶点着色器和片段着色器,并生成最后的着色器[程序] */
    shaderProgram = glCreateProgram();  // 【注意】是 `glCreateProgram`
    glAttachShader(shaderProgram, vertexShader);  // 加入顶点着色器
    glAttachShader(shaderProgram, fragmentShader);  // 加入片段着色器
    glLinkProgram(shaderProgram);  // 链接

  同样,创建“程序”也是获取GPU创建的着色器程序的ID,类型也是: unsigned int

unsigned int shaderProgram;

  获取连接的状态,检查连接结果

/* 因为可能出错,所以进行错误检查,也就是判断时候成功链接 */
    glGetShaderiv(shaderProgram, GL_LINK_STATUS, &success);
    if(!success)
    {
        glGetShaderInfoLog(shaderProgram, 512, NULL, infolog);
        qDebug() << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infolog;
    }

【10】删除不需要的编译结果。

  因为程序已经成功生产了,那么之前旧的编译结果我们就不在需要了,因此可以删除。

  C++程序就可以直接调用这个链接好的着色器程序shaderprogram。

 /* 删除已经不需要的编译的结果 */
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

【11】使用着色器程序:

  在paintGL函数中,启动已经生成好的着色器程序直接使用即可:

 /* 使用着色器 */
    glUseProgram(shaderProgram);

  至此,我们已经成功画出了一个带有颜色的三角形,

 

此处添加Qt的着色器使用:XXOpenglWidget.h

#ifndef AXBOPENGLWIDGET_H
#define AXBOPENGLWIDGET_H

#include <QOpenGLWidget>  // 相当于GLFW
#include <QOpenGLFunctions_4_5_Core>  // 相当于 GLAD

class AXBOpenGLWidget : public QOpenGLWidget, QOpenGLFunctions_4_5_Core
{
    Q_OBJECT
public:
    explicit AXBOpenGLWidget(QWidget *parent = nullptr);


protected:
    /* 需要重载的 QOpenGLWidget 中的三个函数 */
    virtual void initializeGL();
    virtual void resizeGL(int w, int h);
    virtual void paintGL();

signals:

public slots:
};

#endif // AXBOPENGLWIDGET_H

实现文件:XXOpenglWidget.cpp

#include "axbopenglwidget.h"
#include "QDebug"

// 顶点数据
float vertices[] = {
    -0.5f, -0.5f, 0.0f,
    0.5f, -0.5f, 0.0f,
    0.0f,  0.5f, 0.0f
};

// 创建 VAO 和 VBO 对象并且赋予 ID
unsigned int VBO, VAO;

// 顶点着色器的源代码,顶点着色器就是把 xyz 原封不动的送出去
const char *vertexShaderSource = "#version 330 core\n"
                                 "layout (location = 0) in vec3 aPos;\n"
                                 "void main()\n"
                                 "{\n"
                                 "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
                                 "}\0";


// 片段着色器的源代码,片段着色器就是给一个固定的颜色
const char *fragmentShaderSource = "#version 330 core\n"
                                   "out vec4 FragColor;\n"
                                   "void main()\n"
                                   "{\n"
                                   "   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
                                   "}\n\0";

// 着色器程序(链接顶点和片段着色器之后生成的)
unsigned int shaderProgram;


AXBOpenGLWidget::AXBOpenGLWidget(QWidget *parent) : QOpenGLWidget(parent)
{

}

/* 首要要执行初始化过程,将函数指针指向显卡内的函数 */
void AXBOpenGLWidget::initializeGL()
{
    initializeOpenGLFunctions();  // 【重点】初始化OpenGL函数,将 Qt 里的函数指针指向显卡的函数(头文件 QOpenGLFunctions_X_X_Core)
    // ===================== VAO | VBO =====================
    // VAO 和 VBO 对象赋予 ID
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    // 绑定 VAO、VBO 对象
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    /* 为当前绑定到 target 的缓冲区对象创建一个新的数据存储(在 GPU 上创建对应的存储区域,并将内存中的数据发送过去)
        如果 data 不是 NULL,则使用来自此指针的数据初始化数据存储
        void glBufferData(GLenum target,  // 需要在 GPU 上创建的目标
                                            GLsizeipter size,  // 创建的显存大小
                                            const GLvoid* data,  // 数据
                                            GLenum usage)  // 创建在 GPU 上的哪一片区域(显存上的每个区域的性能是不一样的)https://registry.khronos.org/OpenGL-Refpages/es3.0/
    */
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    /* 告知显卡如何解析缓冲区里面的属性值
        void glVertexAttribPointer(
                                    GLuint index,  // VAO 中的第几个属性(VAO 属性的索引)
                                    GLint size,  // VAO 中的第几个属性中对应的位置放几份数据
                                    GLEnum type,  // 存放数据的数据类型
                                    GLboolean normalized,  // 是否标准化
                                    GLsizei stride,  // 步长
                                    const void* offset  // 偏移量
        )
    */
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);  // 第一个属性,所以不需要偏移
    // 开始 VAO 管理的第一个属性值
    glEnableVertexAttribArray(0);
    // 解绑 VAO 和 VBO
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
    // ===================== 顶点着色器 =====================
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);  // 创建顶点着色器(框架 | 对象)并给予编号
    /* 绑定至着色器原码
        void glShaderSource(
                             GLuint shader,  着色器框架
                             GLsize count,  着色器字符串的数量
                             const CLchar** string,  着色器原码字符串
                             const GLint* length  着色器原码的长度,如果是单个字符串可以填 NULL(代表原码字符串以 NULL 结尾)
        )

    */
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);  // 绑定至着色器原码
    glCompileShader(vertexShader);  // 编译着色器

    /* 因为可能出错,所以进行错误检查,也就是判断是否成功编译 */
    int success;  // 是否成功的标志
    char infolog[512];  // 错误日志(信息)
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if(!success)
    {
        glGetShaderInfoLog(vertexShader, 512, NULL, infolog);
        qDebug() << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infolog;
    }
    // ===================== 片段着色器 =====================
    unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);

    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infolog);
        qDebug() << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infolog;
    }
    // ===================== 链接着色器 =====================
    /* 链接顶点着色器和片段着色器,并生成最后的着色器[程序] */
    shaderProgram = glCreateProgram();  // 【注意】是 `glCreateProgram`
    glAttachShader(shaderProgram, vertexShader);  // 加入顶点着色器
    glAttachShader(shaderProgram, fragmentShader);  // 加入片段着色器
    glLinkProgram(shaderProgram);  // 链接

    /* 因为可能出错,所以进行错误检查,也就是判断时候成功链接 */
    glGetShaderiv(shaderProgram, GL_LINK_STATUS, &success);
    if(!success)
    {
        glGetShaderInfoLog(shaderProgram, 512, NULL, infolog);
        qDebug() << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infolog;
    }

    /* 删除已经不需要的编译的结果 */
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
}

void AXBOpenGLWidget::resizeGL(int w, int h)
{
}

void AXBOpenGLWidget::paintGL()
{
    /* 设置 OpenGLWidget 控件背景颜色为深青色 */
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);  // set方法【重点】如果没有 initializeGL,目前是一个空指针状态,没有指向显卡里面的函数,会报错
    glClear(GL_COLOR_BUFFER_BIT);  // use方法

    /* 重新绑定 VAO */
    glBindVertexArray(VAO);

    /* 绘制三角形 */
    glDrawArrays(GL_TRIANGLES, 0, 3);

    /* 使用着色器 */
    glUseProgram(shaderProgram);
}

 

 

7. 抽象着色器类:方便管理Opengl各种着色器,编写,编译等。

  编写、编译、管理着色器是件麻烦事。在着色器主题的最后,我们会写一个类来让我们的生活轻松一点,它可以从硬盘读取着色器,然后编译并链接它们,并对它们进行错误检测,这就变得很好用了。这也会让你了解该如何封装目前所学的知识到一个抽象对象中。

   我们会把着色器类全部放在在头文件里,主要是为了学习用途,当然也方便移植。我们先来添加必要的include,并定义类结构

 

#ifndef SHADER_H
#define SHADER_H

#include <string>
#include <fstream>
#include <sstream>
#include <iostream>

#include <GL/glew.h>; // 包含glew来获取所有的必须OpenGL头文件

class Shader
{
public:
    // 程序ID
    GLuint Program;//着色器程序,从GPU创建的,本质是程序ID
    // 构造器读取并构建着色器
    Shader(const GLchar* vertexPath, const GLchar* fragmentPath);
    // 使用程序
    void Use();
};

#endif

  在上面,我们在头文件顶部使用了几个预处理指令(Preprocessor Directives)。这些预处理指令会告知你的编译器只在它没被包含过的情况下才包含和编译这个头文件,即使多个文件都包含了这个着色器头文件。它是用来防止链接冲突的。

   着色器类储存了着色器程序的ID。它的构造器需要顶点和片段着色器源代码的文件路径,这样我们就可以把源码的文本文件储存在硬盘上了。我们还添加了一个Use函数,它其实不那么重要,但是能够显示这个自造类如何让我们的生活变得轻松

   在构造函数中,完成:着色器顶点着色器程序文件地址和片段着色器程序文件地址处理,并存储在string对象中;

我们使用C++文件流读取着色器内容,储存到几个string对象里:

Shader(const GLchar* vertexPath, const GLchar* fragmentPath)
{
    // 1. 从文件路径中获取【顶点着色器】和【片段着色器】
    std::string vertexCode;
    std::string fragmentCode;
    std::ifstream vShaderFile;//打开 顶点着色器文件的 输入文件流对象
    std::ifstream fShaderFile;// 用于读物片段着色器文件的对象
    // 保证ifstream对象可以抛出异常:
    vShaderFile.exceptions(std::ifstream::badbit);
    fShaderFile.exceptions(std::ifstream::badbit);
    try 
    {
        // 打开文件
        vShaderFile.open(vertexPath);
        fShaderFile.open(fragmentPath);
        std::stringstream vShaderStream, fShaderStream;
        // 读取文件的缓冲内容到流中
        vShaderStream << vShaderFile.rdbuf();
        fShaderStream << fShaderFile.rdbuf();       
        // 关闭文件
        vShaderFile.close();
        fShaderFile.close();
        // 转换流至GLchar数组
        vertexCode = vShaderStream.str();// 将文件内容转化为:string内存对象
        fragmentCode = fShaderStream.str();     
    }
    catch(std::ifstream::failure e)
    {
        std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
    }
    const GLchar* vShaderCode = vertexCode.c_str();
    const GLchar* fShaderCode = fragmentCode.c_str();
    [...]

  下一步,我们需要编译和链接着色器。注意,我们也将检查编译/链接是否失败,如果失败则打印编译时错误,调试的时候这些错误输出会及其重要(你总会需要这些错误日志的):

 

// 2. 编译着色器
GLuint vertex, fragment;
GLint success;
GLchar infoLog[512];

// 创建顶点着色器
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);//关联顶点着色器内容
glCompileShader(vertex);//编译顶点着色器
// 打印编译错误(如果有的话)
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);//查看顶点着色器编译结果:
if(!success)
{
    glGetShaderInfoLog(vertex, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
};

// 片段着色器也类似
[...]

// 着色器程序,并将顶点着色器内容和片段着色器内容关联
this->Program = glCreateProgram();//创建着色器程序,获取着色器程序ID。
glAttachShader(this->Program, vertex);//关联编译后的“顶点着色器”
glAttachShader(this->Program, fragment);//关联编译后的“片段着色器”
glLinkProgram(this->Program);//完成着色器程序的连接过程
// 打印连接错误(如果有的话)
glGetProgramiv(this->Program, GL_LINK_STATUS, &success);//查看着色器链接过程结果
if(!success)
{
    glGetProgramInfoLog(this->Program, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
//连接成功后,原来创建并编译后的着色器可以删除,不再需要了。
// 删除着色器,它们已经链接到我们的程序中了,已经不再需要了
glDeleteShader(vertex);
glDeleteShader(fragment);

最后我们也会实现Use函数:

 

void Use()
{
    glUseProgram(this->Program);
}

现在我们就写完了一个完整的着色器类。使用这个着色器类很简单;只要创建一个着色器对象,从那一点开始我们就可以开始使用了:

 

Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.frag");
...
while(...)
{
    ourShader.Use();
    glUniform1f(glGetUniformLocation(ourShader.Program, "someUniform"), 1.0f);
    DrawStuff();
}

我们把顶点和片段着色器储存为两个叫做shader.vsshader.frag的文件。你可以使用自己喜欢的名字命名着色器文件;我自己觉得用.vs.frag作为扩展名很直观。

 其中:顶点着色器程序:shader.vs

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec3 color;

out vec3 ourColor;

void main()
{
    gl_Position = vec4(position, 1.0f);
    ourColor = color;
}

片段着色器程序:shader.frag

#version 330 core
in vec3 ourColor;

out vec4 color;

void main()
{
    color = vec4(ourColor, 1.0f);
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

endl;

posted on 2025-07-09 14:50  夏天/isummer  阅读(141)  评论(0)    收藏  举报