OpenGL 04--GLSL、数据类型、Uniform、着色器类

一、着色器

在 OpenGL 中,着色器(Shader)是运行在 GPU 上的程序,用于处理图形渲染管线中的不同阶段。

这些小程序为图形渲染管线的某个特定部分而运行。从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。

OpenGL 着色器通常分为几种类型,每种类型负责处理特定的渲染任务。以下是一些常见的着色器类型及其作用:

  1. 顶点着色器(Vertex Shader)

    • 处理每个顶点的数据。

    • 进行顶点位置的变换,包括模型变换、视图变换和投影变换。

    • 计算光照、阴影和其它与顶点相关的属性。

    • 输出用于片元着色器的插值(如纹理坐标、法线方向等)。

  2. 片元着色器(Fragment Shader)

    • 处理每个片元的每个像素的数据。

    • 计算最终的像素颜色值,包括纹理采样、颜色混合和透明度。

    • 应用后处理效果,如模糊、色调映射等。

  3. 几何着色器(Geometry Shader)(可选):

    • 处理图元的顶点数据。

    • 允许创建新的图元(如将三角形转换为线框)或修改现有图元的顶点。

    • 可以增加或减少渲染的顶点数量。

  4. 曲面细分着色器(Tessellation Shader)(可选):

    • 控制曲面细分的过程,用于生成平滑的曲面。

    • 定义细分后的顶点位置和其它相关数据。

  5. 计算着色器(Compute Shader)(可选):

    • 执行通用计算任务,不直接参与渲染管线。

    • 可以用于处理大量数据并行计算,如物理模拟、图像处理等。

二、GLSL

在 OpenGL 程序中,着色器通常以 GLSL(OpenGL Shading Language)编写,这是一种类似于 C/C 的高级着色语言。着色器程序需要编译和链接到着色器程序(Shader Program)中,然后才能被 OpenGL 使用。

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

着色器的开头总是要声明版本,接着是输入和输出变量、uniform和main函数。每个着色器的入口点都是main函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。

一个典型的着色器结构:
cpp 复制代码
// 指定使用的 GLSL 版本号
#version version_number

// 定义一个输入变量,其数据类型为 type,名称为 in_variable_name
// 该变量在顶点着色器中用于接收顶点数据
in type in_variable_name;

// 定义另一个输入变量,其数据类型为 type,名称为 in_variable_name
// 该变量在顶点着色器中用于接收顶点数据
in type in_variable_name;

// 定义一个输出变量,其数据类型为 type,名称为 out_variable_name
// 该变量在顶点着色器中用于输出处理后的顶点数据
out type out_variable_name;

// 定义一个统一变量,其数据类型为 type,名称为 uniform_name
// 该变量在顶点着色器中用于存储着色器程序中全局的数据
uniform type uniform_name;

// 顶点着色器的主函数
void main()
{
  // 在这里处理输入变量,并进行一些图形操作
  // 例如,应用变换、计算光照、纹理坐标等
  ...
  
  // 将处理过的结果输出到输出变量
  out_variable_name = weird_stuff_we_processed;
}

应用示例:

cpp 复制代码
#version 400 core

layout (location = 0) in vec3 aPos;  // 顶点位置
layout (location = 1) in vec3 aColor;  // 顶点颜色

out vec4 outColor;  // 输出颜色

uniform mat4 matModel;  // 模型变换矩阵
uniform mat4 matView;  // 视图变换矩阵
uniform mat4 matProjection;  // 投影变换矩阵

void main()
{
    gl_Position = matProjection * matView * matModel * vec4(aPos, 1.0);  // 应用变换矩阵
    outColor = aColor;  // 输出颜色
}

在这个示例中:

  • aPosaColor 是输入变量,分别表示顶点的位置和颜色。

  • outColor 是输出变量,表示处理后的颜色。

  • matModelmatViewProjection 是统一变量,分别表示模型变换矩阵、视图变换矩阵和投影变换矩阵。

  • main 函数中,我们应用变换矩阵计算变换后的顶点位置,并将顶点颜色直接输出。

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

cpp 复制代码
int nrAttributes;  // 定义一个整数变量,用于存储最大顶点属性数量
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);  // 查询 OpenGL 实现支持的最大顶点属性数量
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;  // 输出最大顶点属性数量

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

三、数据类型

和其他编程语言一样,GLSL有数据类型可以来指定变量的种类。GLSL中包含C等其它语言大部分的默认基础数据类型:intfloatdoubleuintbool。GLSL也有两种容器类型,分别是向量(Vector)和矩阵(Matrix)。

向量

GLSL中的向量是一个可以包含有2、3或者4个分量的容器,分量的类型可以是前面默认基础类型的任意一个。它们可以是下面的形式(n代表分量的数量):

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

大多数时候使用vecn,因为float足够满足大多数要求了。

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

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

cpp 复制代码
vec2 someVec; // 声明一个包含两个分量的二维向量
vec4 differentVec = someVec.xyxx; // 尝试从 someVec 中提取 x 和 y 分量,并添加额外的 x 和 x 分量
vec3 anotherVec = differentVec.zyw; // 尝试从 differentVec 中提取 z、y 和 w 分量
vec4 otherVec = someVec.xxxx + anotherVec.yxzy; // 尝试从 someVec 中提取四个 x 分量,并与 anotherVec 的 y、z 和 y 分量相加
  1. vec4 differentVec = someVec.xyxx; 这一行试图从 vec2 类型的 someVec 中提取两个分量,然后错误地添加了两个额外的 x 分量。在 GLSL 中,你只能通过 .xy.xz 等来访问向量的分量。如果你想要复制 someVecxy 分量到 differentVec,应该是 vec4 differentVec = vec4(someVec, 0.0, 0.0, 1.0);

  2. vec3 anotherVec = differentVec.zyw; 这一行试图从 vec4 类型的 differentVec 中提取三个分量,这是不允许的。vec3 类型只能有三个索引(.x.y.z)。如果你想要从 vec4 类型中提取 zy 分量到 vec3 类型,应该是 vec3 anotherVec = vec3(differentVec.z, differentVec.y, 0.0);

  3. vec4 otherVec = someVec.xxxx + anotherVec.yxzy; 这一行试图访问 someVec 的四个 x 分量,这是不正确的,因为 vec2 类型只有两个分量。另外,anotherVec.yxzy 试图访问 vec3 类型的 anotherVecxyz 分量,但是没有 vec4 类型,所以不能进行加法操作。如果你想要将 someVecxy 分量与 anotherVecyz 分量进行组合,应该是 vec4 otherVec = vec4(someVec.x, someVec.y, anotherVec.y, anotherVec.z);

修:

cpp 复制代码
vec2 someVec; // 声明一个包含两个分量的二维向量
vec4 differentVec = vec4(someVec, 0.0, 0.0, 1.0); // 复制 someVec 的 x 和 y 分量到 differentVec 的前两个分量

vec3 anotherVec = vec3(differentVec.z, differentVec.y, 0.0); // 提取 differentVec 的 z 和 y 分量到 anotherVec
vec4 otherVec = vec4(someVec.x, someVec.y, anotherVec.y, anotherVec.z); // 组合 someVec 和 anotherVec 的分量

上述代码中的变量 someVecdifferentVecanotherVec 需要在着色器中被赋予具体的值,否则它们将包含未定义的值。

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

cpp 复制代码
vec2 vect = vec2(0.5, 0.7); // 创建一个二维向量 vect 并初始化其 x 分量为 0.5,y 分量为 0.7
vec4 result = vec4(vect, 0.0, 0.0); // 创建一个四维向量 result,并将 vect 的 x 和 y 分量复制到 result 的前两个分量,z 和 w 分量初始化为 0.0
vec4 otherResult = vec4(result.xyz, 1.0); // 创建一个新的四维向量 otherResult,其 x、y 和 z 分量来自 result,w 分量设置为 1.0

使用场景

这种类型的操作在图形程序中非常有用,特别是在处理顶点数据、变换和光照计算时。例如,vec4 向量常用于表示齐次坐标(包括位置和齐次坐标 w),这在进行投影变换时非常有用。

注意事项

  • 在 GLSL 中,向量的分量访问(如 result.xyz)返回一个新的向量,包含原向量的指定分量。

  • 当你从一个四维向量中提取三个分量并创建一个新的四维向量时,你需要显式地指定第四个分量(在这个例子中是 1.0)。

  • 这种操作在顶点着色器中特别有用,因为顶点着色器可以接收位置、颜色、纹理坐标等作为输入,然后对这些数据进行变换和计算。

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

四、输入和输出

虽然着色器是各自独立的小程序,但是它们都是一个整体的一部分,出于这样的原因,我们希望每个着色器都有输入和输出,这样才能进行数据交流和传递。GLSL定义了inout关键字专门来实现这个目的。每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。但在顶点和片段着色器中会有点不同。

顶点着色器 应该接收的是一种特殊形式的输入,否则就会效率低下。顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,使用location这一元数据指定输入变量,这样我们才可以在CPU上配置顶点属性。我们已经在前面看过这个了,layout (location = 0)。顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据。

你也可以忽略layout (location = 0)标识符,通过在OpenGL代码中使用glGetAttribLocation查询属性位置值(Location)。

cpp 复制代码
// 指定使用的 GLSL 版本号和核心配置文件
#version 400 core

// 定义一个输入变量,其数据类型为 vec3(三个浮点数),名称为 aPos
// 这个变量在顶点着色器中用于接收顶点位置数据
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0

// 定义一个输出变量,其数据类型为 vec4(四个浮点数),名称为 vertexColor
// 这个变量在顶点着色器中用于输出颜色数据,该数据将传递给片元着色器
out vec4 vertexColor; // 为片段着色器指定一个颜色输出

// 顶点着色器的主函数
void main()
{
    // 设置 gl_Position,这是顶点的最终位置
    // 我们如何把一个 vec3 作为 vec4 的构造器的参数,第四个分量默认为 1.0
    gl_Position = vec4(aPos, 1.0); // 将顶点位置转换为齐次坐标
    
    // 把输出变量设置为暗红色
    // 注意:颜色值的顺序是 RGBA(红、绿、蓝、透明度)
    vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
}

在 OpenGL 程序中用于顶点着色器阶段,负责处理顶点位置和颜色。通过设置 gl_Position,它将顶点位置转换为齐次坐标,以便在视图变换和投影变换后正确渲染顶点。同时,通过设置 vertexColor,它为每个顶点指定一个颜色值,这些颜色值将传递给片元着色器,用于确定每个片元的最终颜色。

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

所以,如果我们打算从一个着色器向另一个着色器发送数据,必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。

cpp 复制代码
// 指定使用的 GLSL 版本号和核心配置文件
#version 400 core

// 定义一个输出变量,其数据类型为 vec4(四个浮点数),名称为 FragColor
// 这个变量在片段着色器中用于输出颜色数据,该数据将用于最终的像素颜色
out vec4 FragColor;

// 定义一个输入变量,其数据类型为 vec4(四个浮点数),名称为 vertexColor
// 这个变量在片段着色器中用于接收从顶点着色器传递过来的颜色数据
in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)

// 片段着色器的主函数
void main()
{
    // 将从顶点着色器接收到的颜色值直接赋值给输出变量 FragColor
    FragColor = vertexColor;
}

可以看到我们在顶点着色器中声明了一个vertexColor变量作为vec4输出,并在片段着色器中声明了一个类似的vertexColor。由于它们名字相同且类型相同,片段着色器中的vertexColor就和顶点着色器中的vertexColor链接了。

五、Uniform

Uniform是另一种从我们的应用程序在 CPU 上传递数据到 GPU 上的着色器的方式,但uniform和顶点属性有些不同。

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

要在 GLSL 中声明 uniform,只需在着色器中使用 uniform 关键字,并带上类型和名称。从那时起,我们就可以在着色器中使用新声明的 uniform。

cpp 复制代码
// 指定使用的 GLSL 版本号和核心配置文件
#version 400 core

// 定义一个输出变量,其数据类型为 vec4(四个浮点数),名称为 FragColor
// 这个变量在片段着色器中用于输出颜色数据,该数据将用于最终的片段(像素)颜色
out vec4 FragColor;

// 定义一个统一变量,其数据类型为 vec4(四个浮点数),名称为 ourColor
// 这个变量在片段着色器中用于存储从OpenGL程序代码中传递的颜色值
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量

// 片段着色器的主函数
void main()
{
    // 将统一变量 ourColor 的值赋给输出变量 FragColor
    // 这意味着每个片段(像素)都将使用 ourColor 指定的颜色
    FragColor = ourColor;
}

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

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

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

cpp 复制代码
// 获取当前时间值,通常用于动画效果
float timeValue = glfwGetTime();

// 计算绿色分量的值,使其在0.0到1.0之间变化,创建一个周期性的效果
// 这里使用了正弦函数(sin)来生成周期性变化,并将其范围限制在0.0到1.0之间
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;

// 获取着色器程序中名为 "ourColor" 的uniform变量的位置
// 这个位置用于后续通过 glUniform 函数设置uniform变量的值
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");

// 激活(使用)名为 shaderProgram 的着色器程序
// 这确保后续的 glUniform 调用更新的是正确的着色器程序中的uniform变量
glUseProgram(shaderProgram);

// 更新着色器程序中 "ourColor" uniform变量的值
// 这里设置红色和蓝色分量为0.0,绿色分量为之前计算的 greenValue,透明度(Alpha)为1.0
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地址不要求你之前使用过着色器程序,但是更新一个uniform之前你必须 先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的。

因为OpenGL在其核心是一个C库,所以它不支持类型重载,在函数参数不同的时候就要为其定义新的函数;glUniform是一个典型例子。这个函数有一个特定的后缀,标识设定的uniform的类型。可能的后缀有:

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

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

如果打算让颜色慢慢变化,就要在游戏循环的每一次迭代中(所以他会逐帧改变)更新这个uniform,否则三角形就不会改变颜色。下面计算greenValue然后每个渲染迭代都更新这个uniform:

cpp 复制代码
while(!glfwWindowShouldClose(window))
{
    // 输入
    processInput(window);

    // 渲染
    // 清除颜色缓冲
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // 记得激活着色器
    glUseProgram(shaderProgram);

    // 更新uniform颜色
    float timeValue = glfwGetTime();
    float greenValue = sin(timeValue) / 2.0f + 0.5f;
    int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
    glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

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

    // 交换缓冲并查询IO事件
    glfwSwapBuffers(window);
    glfwPollEvents();
}
cpp 复制代码
while(!glfwWindowShouldClose(window)) // 当窗口没有接收到关闭信号时持续循环
{
    // 处理用户输入,例如按键或鼠标移动
    processInput(window);

    // 渲染指令开始
    // 清除颜色缓冲区,设置清屏颜色为 RGB(0.2, 0.3, 0.3),Alpha 为 1.0(不透明)
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    // 清除当前激活的缓冲区(颜色缓冲区、深度缓冲区等)
    glClear(GL_COLOR_BUFFER_BIT);

    // 激活(使用)之前编译好的着色器程序
    glUseProgram(shaderProgram);

    // 更新着色器程序中的 uniform 变量 ourColor,以动态改变颜色
    float timeValue = glfwGetTime(); // 获取当前时间
    float greenValue = sin(timeValue) / 2.0f + 0.5f; // 计算绿色分量值
    int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor"); // 获取 uniform 变量位置
    glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); // 设置 uniform 变量的值

    // 绘制三角形
    // 绑定顶点数组对象,它包含了顶点数据和顶点属性的配置
    glBindVertexArray(VAO);
    // 绘制数组中的顶点,GL_TRIANGLES 表示绘制三角形
    glDrawArrays(GL_TRIANGLES, 0, 3);

    // 交换前后缓冲区,将渲染结果展示给用户
    glfwSwapBuffers(window);
    // 处理所有待处理的事件,例如键盘输入、鼠标移动等
    glfwPollEvents();
}

这段代码中,glfwWindowShouldClose(window) 函数检查用户是否尝试关闭窗口(例如,点击窗口的关闭按钮或按下 ESC 键)。只要窗口没有关闭,就继续执行循环体内的代码。

在循环体内,首先处理用户输入,然后开始渲染过程。使用 glClearColorglClear 函数清除颜色缓冲区。接着,激活之前编译好的着色器程序,并更新其中的 ourColor 变量,使其动态变化,从而创建一个颜色随时间变化的效果。

然后,绑定顶点数组对象(VAO)并绘制三角形。最后,交换前后缓冲区,使渲染结果可见,并处理所有待处理的事件。

完整代码:

cpp 复制代码
#include <glad/glad.h>  // 包含GLAD库的头文件,用于加载OpenGL函数指针
#include <GLFW/glfw3.h>  // 包含GLFW库的头文件,用于创建窗口和管理输入
#include <iostream>  // 包含标准输入输出流库
#include <cmath>  // 包含数学库,用于sin函数

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;  // 窗口高度

// 定义顶点着色器的GLSL源码
const char* vertexShaderSource = "#version 400 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main()\n"
"{\n"
"   gl_Position = vec4(aPos, 1.0);\n"
"}\0";

// 定义片段着色器的GLSL源码
const char* fragmentShaderSource = "#version 400 core\n"
"out vec4 FragColor;\n"
"uniform vec4 ourColor;\n"
"void main()\n"
"{\n"
"   FragColor = ourColor;\n"
"}\n\0";

int main()
{
    // 初始化GLFW并配置
    // ----------------------
    glfwInit();  // 初始化GLFW库
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);  // 设置GLFW创建OpenGL 3的上下文
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);  // 设置次要版本号为3
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);  // 设置OpenGL核心配置文件

#ifdef __APPLE__  // 苹果系统需要设置为向前兼容模式
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

    // 创建GLFW窗口
    // --------------------
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)  // 如果窗口创建失败
    {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();  // 终止GLFW
        return -1;
    }
    glfwMakeContextCurrent(window);  // 设置当前上下文为新创建的窗口
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);  // 设置窗口大小改变的回调函数

    // 加载OpenGL函数指针
    // ---------------------------------------
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))  // 使用GLAD加载OpenGL函数指针
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }

    // 构建并编译我们的着色器程序
    // ------------------------------------
    // 顶点着色器
    unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    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);
        std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    // 片段着色器
    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);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    // 链接着色器
    unsigned int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
    // 检查链接错误
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    // 设置顶点数据(和缓冲区)并配置顶点属性
    // ------------------------------------------------------------------
    float vertices[] = {
         0.5f, -0.5f, 0.0f,  // 底部右侧
        -0.5f, -0.5f, 0.0f,  // 底部左侧
         0.0f,  0.5f, 0.0f   // 顶部 
    };

    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    // 首先绑定顶点数组对象,然后绑定并设置顶点缓冲区,然后配置顶点属性。
    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    // 你可以在之后解绑顶点数组对象,这样其他顶点数组对象的调用就不会意外修改这个对象,但这很少发生。修改其他
    // 顶点数组对象需要调用 glBindVertexArray,所以我们通常不需要解绑顶点数组对象(VAO)(VAOs)(或 VBOs)当它不是直接必要时。
    // glBindVertexArray(0);

    // 绑定顶点数组对象(它已经被绑定,但只是为了演示):看到我们只有一个顶点数组对象我们
    // 可以在渲染相应的三角形之前绑定它;这是另一种方法。
    glBindVertexArray(VAO);

    // 渲染循环
    // -----------
    while (!glfwWindowShouldClose(window))  // 当窗口没有接收到关闭信号时持续循环
    {
        // 输入
        processInput(window);  // 处理用户输入

        // 渲染
        // ------
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);  // 清除颜色缓冲区,设置清屏颜色为 RGB(0.2, 0.3, 0.3),Alpha 为 1.0(不透明)
        glClear(GL_COLOR_BUFFER_BIT);  // 清除当前激活的缓冲区(颜色缓冲区、深度缓冲区等)

        // 确保在任何 glUniform 调用之前激活着色器
        glUseProgram(shaderProgram);  // 激活(使用)名为 shaderProgram 的着色器程序

        // 更新着色器uniform颜色
        double timeValue = glfwGetTime();  // 获取当前时间
        float greenValue = static_cast<float>(sin(timeValue) / 2.0 + 0.5);  // 计算绿色分量值
        int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");  // 获取 uniform 变量位置
        glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);  // 设置 uniform 变量的值

        // 绘制三角形
        glBindVertexArray(VAO);  // 绑定顶点数组对象
        glDrawArrays(GL_TRIANGLES, 0, 3);  // 绘制数组中的顶点,GL_TRIANGLES 表示绘制三角形

        // GLFW:交换缓冲区并轮询IO事件(按键按下/释放、鼠标移动等)
        glfwSwapBuffers(window);  // 交换前后缓冲区,将渲染结果展示给用户
        glfwPollEvents();  // 处理所有待处理的事件
    }

    // 可选:一旦它们的目的已经达到,就释放所有资源:
    // --------------------------------------------------------------------
    glDeleteVertexArrays(1, &VAO);  // 删除顶点数组对象
    glDeleteBuffers(1, &VBO);  // 删除缓冲区对象
    glDeleteProgram(shaderProgram);  // 删除着色器程序

    // 终止 GLFW,清除所有先前分配的 GLFW 资源。
    // ------------------------------------------------------------------
    glfwTerminate();  // 终止 GLFW
    return 0;
}

// 处理所有输入:查询 GLFW 本帧是否有相关按键被按下/释放,并相应地做出反应
// ----------------------------------------------------------------------------------------------------------
void processInput(GLFWwindow* window) {
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)  // 如果按下了 ESC 键
        glfwSetWindowShouldClose(window, true);  // 设置窗口关闭标志为真
}

// GLFW:每当窗口大小改变(由操作系统或用户调整大小时)此回调函数执行
// ---------------------------------------------------------------------------------------------
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
    // 确保视口与新窗口尺寸匹配;注意,在视网膜显示器上宽度和高度将明显大于指定值。
    glViewport(0, 0, width, height);  // 设置视口大小
}

六、顶点添加颜色数据

把颜色数据加进顶点数据中(把颜色数据添加为3个float值至vertices数组)。把三角形的三个角分别指定为红色、绿色和蓝色:

cpp 复制代码
float 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标识符来把aColor属性的位置值设置为1:

cpp 复制代码
#version 400 core
layout (location = 0) in vec3 aPos;   // 位置变量的属性位置值为 0 
layout (location = 1) in vec3 aColor; // 颜色变量的属性位置值为 1

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

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

由于不再使用uniform来传递片段的颜色了,现在使用ourColor输出变量,必须再修改一下片段着色器:

cpp 复制代码
// 指定使用的 GLSL 版本号和核心配置文件
#version 400 core

// 定义一个输出变量,其数据类型为 vec4(四个浮点数),名称为 FragColor
// 这个变量在片段着色器中用于输出颜色数据,该数据将用于最终的片段(像素)颜色
out vec4 FragColor;  

// 定义一个输入变量,其数据类型为 vec3(三个浮点数),名称为 ourColor
// 这个变量在片段着色器中用于接收从顶点着色器传递过来的颜色数据
in vec3 ourColor;

// 片段着色器的主函数
void main()
{
    // 将从顶点着色器接收到的颜色值转换为四维向量,并设置 alpha 分量为 1.0(不透明)
    FragColor = vec4(ourColor, 1.0);
}

因为添加了另一个顶点属性,并且更新了VBO的内存,必须重新配置顶点属性指针。更新后的VBO内存中的数据现在看起来像这样:

知道了现在使用的布局,就可以使用glVertexAttribPointer函数更新顶点格式。

cpp 复制代码
// 位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));
glEnableVertexAttribArray(1);

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

由于现在有了两个顶点属性,我们不得不重新计算步长 值。为获得数据队列中下一个属性值(比如位置向量的下个x分量)我们必须向右移动6个float,其中3个是位置值,另外3个是颜色值。这使我们的步长值为6乘以float的字节数(=24字节)。

同样,这次必须指定一个偏移量。对于每个顶点来说,位置顶点属性在前,所以它的偏移量是0。颜色属性紧随位置数据之后,所以偏移量就是3 * sizeof(float),用字节来计算就是12字节。

运行结果:

这是在片段着色器中进行的所谓片段插值(Fragment Interpolation)的结果。当渲染一个三角形时,光栅化(Rasterization)阶段通常会造成比原指定顶点更多的片段。光栅会根据每个片段在三角形形状上所处相对位置决定这些片段的位置。

基于这些位置,它会插值(Interpolate)所有片段着色器的输入变量。比如,有一个线段,上面的端点是绿色的,下面的端点是蓝色的。如果一个片段着色器在线段的70%的位置运行,它的颜色输入属性就会是一个绿色和蓝色的线性结合;更精确地说就是30%蓝 + 70%绿。

七、着色器类

编写、编译、管理着色器是件麻烦事,写一个类会轻松一点,它可以从硬盘读取着色器,然后编译并链接它们,并对它们进行错误检测。

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

cpp 复制代码
#ifndef SHADER_H
#define SHADER_H

// 防止头文件重复包含

#include <glad/glad.h>  // 错误:此处多余的分号(需删除)
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>

class Shader {
public:
    unsigned int ID;  // OpenGL着色器程序对象的ID

    // 构造函数:通过顶点和片段着色器文件路径创建着色器程序
    Shader(const char* vertexPath, const char* fragmentPath);

    // 激活当前着色器程序
    void use();

    // 设置Uniform变量的工具函数(const成员函数,不修改对象状态)
    void setBool(const std::string &name, bool value) const;
    void setInt(const std::string &name, int value) const;
    void setFloat(const std::string &name, float value) const;
};

#endif

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

着色器类储存了着色器程序的ID。它的构造器需要顶点和片段着色器源代码的文件路径,这样就可以把源码的文本文件储存在硬盘上了。除此之外,还加入了一些工具函数:use用来激活着色器程序,所有的set...函数能够查询一个unform的位置值并设置它的值。

从文件读取

① 使用C++文件流读取着色器内容,储存到几个string对象里。
cpp 复制代码
Shader(const char* vertexPath, const char* fragmentPath)
{
    // 1. 从文件路径中获取顶点/片段着色器
    std::string vertexCode;
    std::string fragmentCode;
    std::ifstream vShaderFile;
    std::ifstream fShaderFile;
    // 保证ifstream对象可以抛出异常:
    vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
    fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
    try 
    {
        // 打开文件
        vShaderFile.open(vertexPath);
        fShaderFile.open(fragmentPath);
        std::stringstream vShaderStream, fShaderStream;
        // 读取文件的缓冲内容到数据流中
        vShaderStream << vShaderFile.rdbuf();
        fShaderStream << fShaderFile.rdbuf();       
        // 关闭文件处理器
        vShaderFile.close();
        fShaderFile.close();
        // 转换数据流到string
        vertexCode   = vShaderStream.str();
        fragmentCode = fShaderStream.str();     
    }
    catch(std::ifstream::failure e)
    {
        std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
    }
    const char* vShaderCode = vertexCode.c_str();
    const char* fShaderCode = fragmentCode.c_str();
    [...]
②编译和链接着色器。

(注意,我们也将检查编译/链接是否失败,如果失败则打印编译时错误,调试的时候这些错误输出会及其重要)

cpp 复制代码
// 2. 编译着色器
unsigned int vertex, fragment;
int success;
char 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;
};

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

// 着色器程序
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
// 打印连接错误(如果有的话)
glGetProgramiv(ID, GL_LINK_STATUS, &success);
if(!success)
{
    glGetProgramInfoLog(ID, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}

// 删除着色器,它们已经链接到我们的程序中了,已经不再需要了
glDeleteShader(vertex);
glDeleteShader(fragment);
③use函数
cpp 复制代码
void use() 
{ 
    glUseProgram(ID);
}
④uniform的setter函数
cpp 复制代码
// 设置布尔值到着色器的统一变量中
// 参数:
//   name: 统一变量的名称(在着色器代码中定义的变量名)
//   value: 要设置的布尔值
// 注意:
//   OpenGL 中布尔值通常以整数形式传递(true 为 1,false 为 0)
void setBool(const std::string &name, bool value) const
{
    // 调用 OpenGL 的 glUniform1i 函数将布尔值设置到着色器的统一变量中
    // value 被隐式转换为整数(true 转换为 1,false 转换为 0)
    glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); 
}

// 设置整数值到着色器的统一变量中
// 参数:
//   name: 统一变量的名称
//   value: 要设置的整数值
void setInt(const std::string &name, int value) const
{ 
    // 调用 OpenGL 的 glUniform1i 函数将整数值设置到着色器的统一变量中
    glUniform1i(glGetUniformLocation(ID, name.c_str()), value); 
}

// 设置浮点值到着色器的统一变量中
// 参数:
//   name: 统一变量的名称
//   value: 要设置的浮点值
void setFloat(const std::string &name, float value) const
{ 
    // 调用 OpenGL 的 glUniform1f 函数将浮点值设置到着色器的统一变量中
    glUniform1f(glGetUniformLocation(ID, name.c_str()), value); 
}
⑤创建一个着色器对象(Shader 类的实例),并在渲染循环中使用它来设置统一变量并绘制图形
cpp 复制代码
// 创建一个 Shader 对象,指定顶点着色器和片段着色器的文件路径
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");
...
while(...) // 假设这是一个渲染循环,例如 OpenGL 的主循环
{
    // 激活着色器程序
    ourShader.use();

    // 设置着色器中的统一变量 "someUniform" 的值为 1.0f
    ourShader.setFloat("someUniform", 1.0f);

    // 调用绘制函数,绘制图形
    DrawStuff();
}

顶点和片段着色器储存为两个叫做shader.vsshader.fs的文件

练习

4.0.shader.fs

cpp 复制代码
#version 400 core
out vec4 FragColor; // 输出片段颜色

in vec3 ourColor; // 从顶点着色器接收的颜色

void main()
{
    FragColor = vec4(ourColor, 1.0); // 设置片段颜色(添加透明度)
}

4.0.shader.vs

cpp 复制代码
#version 400 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;            // 将顶点颜色传递到片段着色器
}

1.修改顶点着色器让三角形上下颠倒

cpp 复制代码
#version 400 core
layout (location = 0) in vec3 aPos;    // 顶点位置属性
layout (location = 1) in vec3 aColor;  // 顶点颜色属性

out vec3 ourColor;                     // 输出变量,传递给片段着色器

void main()
{
    gl_Position = vec4(aPos.x, -aPos.y, aPos.z, 1.0); // 设置顶点位置,并对 y 分量取反
    ourColor = aColor;                                // 将顶点颜色传递给片段着色器
}

2.使用uniform定义一个水平偏移量,在顶点着色器中使用这个偏移量把三角形移动到屏幕右侧

cpp 复制代码
// In your CPP file:
// ======================
float offset = 0.5f;
ourShader.setFloat("xOffset", offset);

// In your vertex shader:
// ======================
#version 400 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;

out vec3 ourColor;

uniform float xOffset;

void main()
{
    gl_Position = vec4(aPos.x + xOffset, aPos.y, aPos.z, 1.0); // add the xOffset to the x position of the vertex position
    ourColor = aColor;
}
cpp 复制代码
// 设置统一变量的值
    float offset = 0.5f;  // 定义一个偏移量
    ourShader.use();      // 激活着色器程序
    ourShader.setFloat("xOffset", offset);  // 设置统一变量的值
cpp 复制代码
// 指定 GLSL 的版本为 4.00,并使用核心(Core)模式。这是 OpenGL 4.0 及以上版本的标准。
#version 400 core

// 定义顶点位置输入,位于顶点属性位置 0。
layout (location = 0) in vec3 aPos;

// 定义顶点颜色输入,位于顶点属性位置 1。
layout (location = 1) in vec3 aColor;

// 定义一个输出变量,用于将顶点颜色传递给片段着色器。
out vec3 ourColor;

// 定义一个统一变量,用于在运行时动态调整顶点的 x 坐标。
uniform float xOffset;

void main()
{
    // 设置顶点的最终位置。
    // 将 xOffset 的值添加到顶点的 x 坐标上,从而动态调整顶点的水平位置。
    gl_Position = vec4(aPos.x + xOffset, aPos.y, aPos.z, 1.0);

    // 将顶点颜色传递给片段着色器。
    ourColor = aColor;
}

3.使用out关键字把顶点位置输出到片段着色器,并将片段的颜色设置为与顶点位置相等(来看看连顶点位置值都在三角形中被插值的结果)。

vs

cpp 复制代码
#version 400 core
layout (location = 0) in vec3 aPos;       // 顶点位置输入
layout (location = 1) in vec3 aColor;     // 顶点颜色输入

out vec3 ourPosition;                     // 输出到片段着色器的顶点位置

void main()
{
    gl_Position = vec4(aPos, 1.0);        // 设置顶点的最终位置
    ourPosition = aPos;                   // 将顶点位置传递给片段着色器
}

fs

cpp 复制代码
#version 400 core
out vec4 FragColor;                       // 输出片段颜色
in vec3 ourPosition;                      // 从顶着点色器接收的顶点位置

void main()
{
    FragColor = vec4(ourPosition, 1.0);   // 将插值后的顶点位置作为颜色输出
}

Q:为什么三角形的左下角是黑的?

A:片段颜色的输出等于三角形顶点的坐标(插值后)。三角形左下角的坐标是 (-0.5f, -0.5f, 0.0f)。由于 x 和 y 值是负数,它们会被截断为 0.0f 的值。这种情况会一直持续到三角形的中心部分,因为从中心部分开始,插值后的值会再次变为正值。0.0f 的值当然对应黑色。

参考:

着色器 - LearnOpenGL CN

相关推荐
Non importa29 分钟前
【初阶数据结构】线性表之双链表
c语言·开发语言·数据结构·c++·考研·链表·学习方法
长流小哥39 分钟前
Qt 动画学习 入门Qt,怎能少了学动画!
开发语言·qt·学习
学习同学41 分钟前
C++初阶知识复习 (31~45)
开发语言·c++
虾球xz41 分钟前
游戏引擎学习第189天
学习·信息可视化·游戏引擎
泽55318042 分钟前
java学习
学习
每次的天空1 小时前
Android Jetpack学习总结(源码级理解)
android·学习·android jetpack
A1-291 小时前
C++的四种类型转换
开发语言·c++
V---scwantop---信1 小时前
热门索尼S-Log3电影感氛围旅拍LUTS调色预设 Christian Mate Grab - Sony S-Log3 Cinematic LUTs
笔记
Long_poem1 小时前
【自学笔记】PHP语言基础知识点总览-持续更新
android·笔记·php
噜啦噜啦嘞好2 小时前
c++的特性——多态
开发语言·c++