一.概述
在上一篇《绘制一个三角形》中涉及到了顶点着色器、片段着色器,也初步了解了如何编写和使用一个着色器
着色器是OpenGL的灵魂
跟着官方学习文档的脚步,专门写一篇来细致全面地了解着色器
从基本意义上来说,着色器只是一种把输入转化为输出的程序,也是一种非常独立的程序,为图形渲染管线的某个特定部分而运行在GPU上,它们之间不能相互通信
它们之间唯一的沟通只有通过各自的输入和输出
二.GLSL
着色器是使用一种叫GLSL的类C语言写成的,包含一些针对向量和矩阵操作的有用特性
官方参考文档:
https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.1.20.pdf
一个着色器程序的会包含如下几点:
- 声明版本
- 输入变量、输出变量、Uniform全局变量
- main()函数
main()函数处理所有的输入变量,并将结果输出到输出变量
代码段:
#version version_number //声明版本
in type in_variable_name; //输入变量1
in type in_variable_name; //输入变量2
out type out_variable_name; //输出变量
uniform type uniform_name; //全局变量
int main() //main()函数
{
// 处理输入并进行一些图形操作
...
// 输出处理过的结果到输出变量
out_variable_name = weird_stuff_we_processed;
}
对顶点着色器而言,每个输入变量也叫顶点属性
OpenGL中能声明的顶点属性是有上限的,一般由硬件来决定
OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性
可以通过查询 GL_MAX_VERTEX_ATTRIBS 来获取具体的上限:
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
通常情况下会返回16个,大部分情况是够用了
三.数据类型
3.1 GLSL数据类型
基础数据类型:int、float、double、uint、bool
两种容器类型:向量(Vector)、矩阵(Matrix)
3.2 向量
vecn: 包含n个float分量的默认向量
bvecn:包含n个bool分量的向量
ivecn: 包含n个int分量的向量
uvecn:包含n个unsigned int分量的向量
dvecn:包含n个double分量的向量
大多数情况下使用vecn就足够了
GLSL中向量的运算:
获取分量:vec.x、vec.y、vec.z、vec.w
重组运算:
vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;
传参运算:
vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);
四.输入与输出
GLSL定义了 in 和 out 关键字实现着色器的输入与输出
每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去
顶点着色器和片段着色器的输入输出稍有不同:
顶点着色器需要为它的输入提供一个额外的layout标识,这样我们才能把它链接到顶点数据
片段着色器,需要一个vec4颜色输出变量,因为片段着色器需要生成一个最终输出的颜色
如果打算从一个着色器向另一个着色器发送数据
必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入
当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了
示例:让顶点着色器为片段着色器决定颜色
顶点着色器代码:
#version 330 core
layout (location = 0) in vec3 aPos; // 位置变量的属性位置值为0
out vec4 vertexColor; // 为片段着色器指定一个颜色输出
void main()
{
gl_Position = vec4(aPos, 1.0); // 注意我们如何把一个vec3作为vec4的构造器的参数
vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 把输出变量设置为暗红色
}
片段着色器代码:
#version 330 core
out vec4 FragColor;
in vec4 vertexColor; // 从顶点着色器传来的输入变量(名称相同、类型相同)
void main()
{
FragColor = vertexColor;
}
顶点着色器和片段着色器通过变量vertexColor衔接在一起
编译运行:
成功地从顶点着色器向片段着色器发送数据。
让我们更上一层楼,看看能否从应用程序中直接给片段着色器发送一个颜色!
五.Uniform
Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式
Uniform是全局变量,全局变量就意味着:
(1).我们可以在任何着色器中定义它们
(2).在每个着色器程序对象中都是独一无二的,可以被着色器程序的任意着色器在任意阶段访问
(3).无论把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新
示例:通过uniform设置三角形的颜色
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量
void main()
{
FragColor = ourColor;
}
这个Uniform现在还没有被赋值
首先需要使用glGetUniformLocation()函数获取到着色器中Uniform属性的索引/位置值,然后才能更新它的值了
这次不再赋一个固定的颜色值了,而是让它随着时间改变颜色:
//通过glfwGetTime()获取运行的秒数
float timeValue = glfwGetTime();
//使用sin()函数让颜色在0.0到1.0
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
/*查询uniform ourColor的位置值,返回-1表示没能找到
参数:着色器程序、uniform名字*/
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
//设置uniform变量的值
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
查询Uniform地址不要求之前使用过着色器程序
更新一个Uniform之前必须先使用程序(调用glUseProgram),因为它是在当前激活的着色器程序中设置uniform的
OpenGL在其核心是一个C库,所以它不支持类型重载,在函数参数不同的时候就要为其定义新的函数;
glUniform是一个典型例子,这个函数用特定的后缀标识设定的uniform的类型:
f 函数需要一个float作为它的值
i 函数需要一个int作为它的值
ui 函数需要一个unsigned int作为它的值
3f 函数需要3个float作为它的值
fv 函数需要一个float向量/数组作为它的值
知道如何设置uniform变量的值以后
现在开始动态渲染三角形,在渲染循环的每一次迭代中(所以他会逐帧改变)更新这个uniform
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();
}
编译运行,效果如下:
可以看到,uniform对于设置一个在渲染迭代中值会改变的属性是一个非常有用的
十分便于程序和着色器之间进行数据交互
但是假如我们打算为每个顶点设置一个颜色的时候该怎么办?
这种情况下,就不得不声明和顶点数目一样多的uniform了
但是在这一问题上更好的解决方案是在顶点属性中包含更多的数据,
这是我们接下来要做的事情
六.更多属性
颜色数据加进顶点数据中
三角形的三个顶点分别指定为红色、绿色和蓝色:
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 // 顶部
};
顶点数据中以前只有一个"位置属性",现在新增了一个"颜色属性"
顶点着色器的代码也相应进行修改:
#version 330 core
// 位置变量的属性位置值为 0
layout (location = 0) in vec3 aPos;
// 颜色变量的属性位置值为 1
layout (location = 1) in vec3 aColor;
out vec3 ourColor; // 向片段着色器输出一个颜色
void main()
{
gl_Position = vec4(aPos, 1.0);
// 将ourColor设置为我们从顶点数据那里得到的输入颜色
ourColor = aColor;
}
片段着色器的代码也要进行修改:
#version 330 core
in vec3 ourColor;
out vec4 FragColor;
void main()
{
FragColor = vec4(ourColor, 1.0);
}
新增了一个顶点属性,VBO也变化了:
顶点属性指针也需要重新分配:
// 位置属性
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);
参数变化:
- 颜色属性是顶点的第2个属性,所以第一个参数配1
- 一个顶点有两个属性,6个float,所以第五个参数步长配 6*sizeof(float)
- 颜色属性在每个顶点内存的偏移是3个float,所以最后一个参数填 3*sizeof(float)
代码修改好之后,编译运行:
看到这个三角形就会有个疑问:
为什么我们只设置了三个顶点的颜色,但是显示出来的却是调色板三角形?
这是在片段着色器中进行的片段插值(Fragment Interpolation)的结果
当渲染一个三角形时,光栅化(Rasterization)阶段会将三角形分割成无数个像素片段进行上色
上色的原则可以理解为"近朱则赤,近墨者黑",就是采取周围的像素的像素值进行插值填充
这样就造成了调色板的效果
七.封装着色器类
到现在为止,我们所有的代码都写在了一个main.cpp中
着色器的编写、编译、检测、链接、调用等相关代码完全可以单独抽离封装成文件,保证代码的可读性和可移植性
写一个着色器类的头文件 Shader.h:
#include <glad/glad.h>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
using namespace std;
class Shader
{
public:
//着色器程序ID
unsigned int ID;
// 构造器读取并构建着色器
Shader(const char* vertexPath, const char* fragmentPath)
{
//1.从filePath中检索顶点 / 片段源代码
// 顶点和片段着色器代码文件
ifstream vShaderFile;
ifstream fShaderFile;
// 顶点和片段着色器源码字符串
string vertexCode;
string fragmentCode;
// 确保ifstream对象能够抛出异常
vShaderFile.exceptions(ifstream::failbit | ifstream::badbit);
fShaderFile.exceptions(ifstream::failbit | ifstream::badbit);
try
{
// open file
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
// 读取文件缓冲内容到streams
std::stringstream vShaderStream, fShaderStream;
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
// close file hander
vShaderFile.close();
fShaderFile.close();
// stream转换赋值给string字符串,最终string还需要转换成char*指针
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch (std::ifstream::failure& e)
{
std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ: " << e.what() << std::endl;
}
//string转换成char*
const char* vShaderCode = vertexCode.c_str();
const char* fShaderCode = fragmentCode.c_str();
//2.编译shader
//创建顶点着色器
unsigned int vertex;
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
checkCompileErrors(vertex, "VERTEX");
//创建片段着色器
unsigned int fragment;
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
checkCompileErrors(fragment, "FRAGMENT");
//创建着色器程序
ID = glCreateProgram();
//附件着色器到着色器程序,然后链接着色器程序到OpenGL
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
checkCompileErrors(ID, "PROGRAM");
//删除着色器
glDeleteShader(vertex);
glDeleteShader(fragment);
}
// 使用/激活着色器程序
void use()
{
glUseProgram(ID);
}
// uniform工具函数
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);
}
private:
// 检测着色器编译错误和着色器程序链接错误
void checkCompileErrors(unsigned int shader, std::string type)
{
int success;
char infoLog[1024];
if (type != "PROGRAM")
{
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(shader, 1024, NULL, infoLog);
std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n ----------------------------------------------------- -- " << std::endl;
}
}
else
{
glGetProgramiv(shader, GL_LINK_STATUS, &success);
if (!success)
{
glGetProgramInfoLog(shader, 1024, NULL, infoLog);
std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n ----------------------------------------------------- -- " << std::endl;
}
}
}
};
main.cpp文件中关于着色器的代码基本可以删掉了
只需要保留着色器加载和着色器程序使用:
#include "Shader.h"
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
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()
{
// glfw: 初始化窗口配置
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);
#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();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// glad: load OpenGL所有函数指针
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
return -1;
}
//加载着色器文件
Shader ourShader("assets/shader.vs", "assets/shader.fs");
// 配置顶点数据buffer和顶点属性
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 // 顶部
};
unsigned int VBO, VAO;
glGenVertexArrays(1, &VAO);
glGenBuffers(1, &VBO);
// 绑定VAO先,然后绑定和设置顶点buffer,再配置顶点属性
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
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);
//激活着色器程序
ourShader.use();
// 渲染循环
while (!glfwWindowShouldClose(window))
{
// 处理外设输入
processInput(window);
// render
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 绘制三角形
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
// glBindVertexArray(0); // no need to unbind it every time
// glfw: 交换缓冲区和轮询IO事件(按键按下/释放、鼠标移动等)
glfwSwapBuffers(window);
glfwPollEvents();
}
// 删除分配的所有资源:
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
// glfw:终止、清除所有先前分配的GLFW资源。
glfwTerminate();
return 0;
}
// glfw:处理窗口事件
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
// glfw: 窗口尺寸变化回调
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
// 确保视口与新窗口尺寸匹配
glViewport(0, 0, width, height);
}
从代码中可以看到,main.cpp中对着色器的调用就只剩下两处了:
//加载着色器文件
Shader ourShader("assets/shader.vs", "assets/shader.fs");
.....
//激活着色器程序
ourShader.use();
.....
在工程目录下新建了一个assets文件夹
再新建两个txt文本,将顶点和片段着色器的内容保存后,分别修改后缀为vs和fs
名称、后缀可以随便修改,自己喜欢就好
编译运行,结果与之前的一样: