OpenGL中的着色器(Shader)语言,也就是GLSL。可以把它们看作是一个独立的小程序,这个小程序有输入,然后处理输入,然后将处理的结果输出,两个Shader通过输入和输出进行通信,GLSL为此还有专门的关键字:in和 out 。shader发送方将输出规定为特定的类型,shader的接收方接收相同的类型作为自己的输入,这样两个shader就联系了起来。
shader的格式基本固定,首先声明版本,然后定义输入输出的格式以及变量,接着是Uniform和main函数,和C语言一样,shader的入口都是main函数,main函数里面我进行数据的处理,然后将处理的数据传给输出变量。
OpenGL的shader里面支持C语言中的大部分的基础数据结构,int,float,double,bool等,然后shader里面还有自己的容器,分别是:向量(veector)和矩阵(matrix)。
向量:
vec2、vec3、vec4分别可以定义一个二维,三维,四维的向量。
三维向量可以使用二维向量进行填充,四维向量可以使用三维向量进行填充。
他们的分量也可以使用.x .y .z进行访问。
也可以使用一个向量的某些分量去合并成一个新的向量。
vec2 one = vec2(0.2,0.5);
vec3 two = vec3(one,0.8);
vec4 three = vec4(two,1.0);
// 使用three向量的x分量初始化four向量的四个分量
vec4 four = three.xxxx
// 使用four向量的y,y,x,z向量分别初始化five向量的四个分量
vec4 five = four.yyxz;
顶点着色器
顶点着色器从顶点数据中直接接收输入,为了便于顶点数据的管理,OpenGL的shader通过location这一元数据指定输入变量,指定好输入变量之后,为我们才能再CPU上配置顶点属性。顶点着色器需要给他一个特殊的标识:layout,才能将这个标识链接到顶点数据上:layout(location = 0)
片段着色器
片段着色器的输出是vec4的颜色变量,如果你没有在片段着色器在中设置输出变量的话,OpenGL就会把你的物体渲染成黑色.
现在我们准备使用一个着色器向另外一个着色器进行传输数据。在发送方声明一个类型的输出和输出变量,然后在接收方声明一个相同的类型的,相同名字的输入变量。这样两个就可以协同工作了。
顶点着色器的代码:
#version 330 core
// 位置变量的属性位置值为0
// 因为现在顶点数组里面只有顶点的位置信息,所以定义这么一个就行
layout (location = 0) in vec3 aPos;
out vec4 vertexColor; // 为片段着色器指定一个颜色输出
void main()
{
gl_Position = vec4(aPos,1.0);
// 把输出的颜色变量赋值为暗红色
veetexColor = vec4(0.5,0.0,0.0,0.0);
}
片段着色器的代码:
#version 300 core
// 此时的输入变量就是顶点着色器传的输出变量
in vec4 vertexColor;
out vec4 ForgeColor;
void main()
{
ForgeColor = vertexColor;
}
Uniform
Uniform和顶点属性有所不同,Uniform是cpu向gpu传送数据的一种格式,Uniform定义的变量是全局的,这就意味着定义好这个变量之后,在任何着色器的任何阶段都可以去访问这个变量,而且对于每一个着色器片段都是独一无二的,也就是同名的Uniform变量只能定义一次 。
但当你把Uniform定义好之后,Uniform会一直保存他的数据,直至你去更新或者重置他,因为 Uniform是全局的,所以他的生命周期和整个程序的声明周期是一样的。
像下面这样,我们在片段着色器中声明了一个uniform变量,那么他在全局都是可用的,此时就不需要通过顶点着色器作为中介了。
Tip:如果你在GLSL中定义了一个Uniform变量,但是没有使用它,那么编译器会默认的将他移除,导致编译好的版本不会有它,可能会引起一些麻烦,但是文档没有说什么麻烦,注意就ok了。
#version 300 core
out vec4 OutColor;
uniform vec4 ForgeColor;
void main()
{
OutColor = ForgrColor;
}
此时我们怎么在代码中查找对应Uniform的值以及设置他呢?
// 我们使用指令获取运行的时间:s
float timevalue = glfwGetTime();
// 使用sin函数,使得这个值在1.0到0.0之间变化
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
// 我们使用指令获取Uniform值的位置,第一个参数是shader程序,第二个是我们需要获取的Uniform的值
// 当shader里面没有这个Uniform得值的时候,就会返回-1,
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ForgeColor");
// 现在我们 使用查询到的信息来改变Uniform的值
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
Tip:查询Uniform的值的时候,不需要使用shader程序,但是我们更新Uniform的值的时候,我们必须先去使用shader,因为更新数据是在已经激活的shader中进行操作的,所以我们必须先使用:glUseProgram指令激活shader程序。
因为OpenGL在其核心是一个C库,所以它不支持类型重载,在函数参数不同的时候就要为其定义新的函数;glUniform是一个典型例子。这个函数有一个特定的后缀,标识设定的uniform的类型。可能的后缀有:
后缀 | 含义 |
---|---|
f |
函数需要一个float作为它的值 |
i |
函数需要一个int作为它的值 |
ui |
函数需要一个unsigned int作为它的值 |
3f |
函数需要3个float作为它的值 |
fv |
函数需要一个float向量/数组作为它的值 |
每当你打算配置一个OpenGL的选项时就可以简单地根据这些规则选择适合你的数据类型的重载函数。在我们的例子里,我们希望分别设定uniform的4个float值,所以我们通过glUniform4f传递我们的数据
下面的程序我们就在代买里面修改Uniform的值,然后使用这个值进行每一帧的渲染:
while(!glfwWindowShouldClose(window))
{
// 输入
processInput(window);
// 清除颜色缓冲
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER);
// 激活shader程序
glUseProgram(ShaderProgram);
// 设置Uniform的值
float timevalue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ForgeColor");
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
// 绘制三角形
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
// 交换缓冲并查询IO事件
glfwSwapBuffers(window);
glfwPollEvents();
}
总结:
Uniform在更改每一次迭代都会改变的变量的时候,是一个很方便的工具,但是如果我们需要对很多个顶点做操作的时候,此时定义很多的Uniform是不现实的。
如果我们的顶点各有不同的颜色,那我们可以试试下面这种方法。
将顶点的位置属性以及颜色属性都写在顶点数组里面:
float vertex[] =
{
// 位置 // 颜色
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 // 顶部
}
此时顶点数组的属性内容变化了,此时我们的顶点着色器也应该改变一下:
#version 300 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
// 想片段着色器输出的颜色向量
out vec3 ourColor;
void main()
{
gl_Positon = vec4(aPos,1.0);
ourColor = vec4(aColor,1.0);
}
因为我们现在不使用Uniform进行颜色的修改了,此时使用顶点着色器的传出的ourColor,所以片段着色器修改如下:
#version 300 core
out vec4 FragColor;
in vec3 ourColor;
void main()
{
FragColor = vec4(ourColor);
}
因为我们更新了VBO中的结构,所以现在顶点结构像下面这样:
现在我们应该使用glVertexAttribPointer函数进行更新顶点的格式:
我们先来介绍一下glVertexAttribPointer函数:
原型:
void glVertexAttribPointer( GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,const GLvoid * pointer);
参数解析:
index
指定要修改的顶点属性的索引值,这个我理解是:我们自己进行指定的,类似于创建贴图索引差不多
size
指定每个顶点属性的组件数量。必须为1、2、3或者4。初始值为4。(如position是由3个(x,y,z)组成,而颜色是4个(r,g,b,a),这个顶点属性里面颜色只有rgb,所以这里填3)
type
指定数组中每个组件的数据类型。可用的符号常量有GL_BYTE, GL_UNSIGNED_BYTE, GL_SHORT,GL_UNSIGNED_SHORT, GL_FIXED, 和 GL_FLOAT,初始值为GL_FLOAT。
normalized
指定当被访问时,固定点数据值是否应该被归一化(GL_TRUE)或者直接转换为固定点值(GL_FALSE)。
stride
指定连续顶点属性之间的偏移量。如果为0,那么顶点属性会被理解为:它们是紧密排列在一起的。初始值为0。在当前例子中,位置属性有三个元素,中间间隔3个颜色元素的位置,所以两个位置信息之间的间隔是:6*float,两个颜色信息之间的间隔也是6*float;
pointer
指定第一个组件在数组的第一个顶点属性中的偏移量。该数组与GL_ARRAY_BUFFER绑定,储存于缓冲区中。初始值为0;
在当前的例子中,位置信息的偏移量是0,因为开始就是,但是颜色信息的偏移量就是3*float。
所以我们现在再代码中这么更新顶点格式:
// 位置属性
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);
关于里面用到的另外一个指令:glEnableVertexAttribArray
出于性能的考虑,所有顶点着色器的属性变量都是关闭的,意味着数据对于着色器端是不可见到的,哪怕此时数据已经通过shader传到了gpu中。只用通过glEnableVertexAttribArray函数启用属性,才可以在顶点shader中访问对应的属性。
glVertexAttribPointer只是建立CPU和GPU的逻辑链接,但是数据对于GPU是否可见,也就是顶点shader能否读取到数据,是由其是否启用了这个顶点属性来决定的,怎么启用,就是通过glEnableVertexAttribArray函数进行开启的。
搞一波我们自己的着色器类
上面的繁琐操作可以进行一波封装,封装成我们自己的着色器类,然后我们就可以从硬盘去读取,然后编译链接他们,
我们先写一波这个类的头文件:
#pragma once
#include <glad/glad.h>; // 包含glad来获取所有的必须OpenGL头文件
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
class Shader
{
public:
// 程序ID
unsigned int ID;
// 构造器读取并构建着色器
Shader(const GLchar* vertexPath, const GLchar* fragmentPath);
// 使用/激活程序
void use();
// uniform工具函数
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;
};
头文件中:
ID储存的是shader的程序ID
Shader函数是接收两个参数,顶点shader和片段shader的文件路径,对这两个shader进行编译,方便我们储存在硬盘中
use函数可以激活shader程序
set...函数可以查询三种不同类型Uniform数据的位置
下面我们来搞一波Shader函数的实现:
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;
}
// 此时vShadercode 和 fShaderCode 里面就是顶点shader和片段shader中的代码了
const char* vShaderCode = vertexCode.c_str();
const char* fShaderCode = fragmentCode.c_str();
// 2. 编译着色器
unsigned int vertex, fragment;
int success;
char infoLog[512];
// 顶点着色器
// glCreateShader函数创建一个空的着色器对象,并返回一个非零的引用标识
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;
};
// 片段着色器也类似
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if(!success)
{
glGetShaderInfoLog(vertex, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
};
// 着色器程序
// 该函数将创建一个空的program对象,并返回一个非零的引用标识
ID = glCreateProgram();
// 将着色器对象附加到指定的program对象上
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);
}
里面有关C++的函数就不解释了 ,下面是里面使用的一些gl指令的函数原型
1、glCreateShader函数创建一个空的着色器对象,并返回一个非零的引用标识。着色器对象用于维护定义着色器的源代码字符串。可以传入GL_VERTEX_SHADER或GL_FRAGMENT_SHADER来创建顶点着色器和片元着色器。
2、void glShaderSource(GLuint shader,GLsizei count,const GLchar * const *string,const GLint *length);
shader
要被替换源代码的着色器对象的句柄(ID)。也就是我们在上一句指令glCreateShader中返回的标识符。
count
指定字符串和长度数组中的元素数。因为我们将整个shader代码当做一个字符串,所以也可看作是只有一个成员的指针数组
在例子中,我们将它指定为1.
string
指定指向包含要加载到着色器的源代码的字符串的指针数组。
length
指定字符串长度的数组,如果length为NULL,则认为每个字符串都以null结尾。如果length不是NULL,则它指向包含字符串
每个元素的字符串长度的数组
3、glCompileShader会编译已存储在由着色器指定的着色器对象中的源代码字符串,并将编译结果保存
4、该函数将创建一个空的program对象,并返回一个非零的引用标识。program对象可以附加和移除着色器对象,一般包含顶
着色器和片元着色器。program会根据附加的着色器对象创建一些可执行文件,并使之成为当前渲染环境的一部分。
program对象是一个更高层级的对象,可以方便管理我们创建的着色器。
5、glAttachShader将着色器对象附加到指定的program对象上,以便在连接时生成可执行文件。附加操作在着色器的对象生成之前,这就意味着在着色器代码生成编译之前按,你就可以将生成的着色器对象附加到program上面。
6、glLinkProgram执行链接操作,并保存链接状态
7、在glLinkProgram之后应删除已链接的着色器,减少GPU资源占用。通常我们的删除方式是glDeleteShader。
以上是Shader函数的实现,下面其他函数的实现:
use函数,就是激活着色器的使用,里面只是封装了一下激活指令:
void use()
{
glUseProgram(ID);
}
其余的set有关的函数:
void setBool(const std::string &name, bool value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
void setInt(const std::string &name, int value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}
void setFloat(const std::string &name, float value) const
{
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
glGetUniformLocation指令:第一个参数是shader的标识符,第二个是需要查询的Uniform的变量名,然后这个指令返回的是查询Uniform变量在shader中的位置,然后把这个位置信息当做glUniform1i函数或者glUniform1f函数的第一个参数。
glUniform1f设置一个 float类型的Uniform对象的值,第一个参数是位置,由glGetUniformLocation指令返回,第二个参数是要设置的新值。
到这儿我们的Shader类就创建好了:
// 初始化的时候,使用类的构造函数,进行shader对象的创建,绑定,链接,编译等工作
Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");
...
while(...)
{
ourShader.use();
ourShader.setFloat("someUniform", 1.0f);
DrawStuff();
}
备注:GLSL的着色器文件没有固定的文件名。