( 本文对应学习章节:https://learnopengl-cn.github.io/01%20Getting%20started/05%20Shaders/ )
0.前言
在之前绘制三角形的章节中已经初步了解了着色器和着色器语言。在本章节中,教程进一步讲解着色器和着色器语言GLSL,及相关的数据类型,参数传递等。在最后,封装了一个简易的着色器类,简化主体流程。
1.了解着色器与着色器语言
着色器(Shader)是运行在GPU上的小程序。这些小程序为图形渲染管线的某个特定部分而运行。从基本意义上来说,着色器只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。
着色器是使用一种叫GLSL的类C语言写成的,典型的着色器代码结构如下:
#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;
}
数据类型
GLSL中包含常见的基础数据类型:int、float、double、uint和bool。也有两种容器类型:向量(Vector)和矩阵(Matrix)。
向量是一个可以包含至多4个分量的容器,可以看作基本类型的复合形式:vec3就是3个分量的float向量(首字母不带类型默认是float),bvec2表示2个分量的bool向量,ivec4表示4个分量的int向量。
对于向量中的分量,可以通过.xyzw或是.rgba或是.stpq来访问,而且可以灵活的重组和构造:
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标识,这样我们才能把它链接到顶点数据。而layout的location偏移是通过glVertexAttribPointer函数的第一个参数指定的:
glVertexAttribPointer定义通用顶点属性数据的数组:
void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid * pointer);
参数1指定顶点属性的索引;参数2指定顶点属性的分量数,可以是[1-4];参数3指定数据类型;参数4设置是否归一化;参数5指定连续的顶点属性间的字节偏移;参数6指定第一个值的偏移量。可以对照上面的代码示例。
另一个例外是片段着色器,它需要一个vec4颜色输出变量 ,因为片段着色器需要生成一个最终输出的颜色。如果你在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。
Uniform
Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。
假设我们在片段着色器声明了一个uniform的颜色向量:
uniform vec4 ourColor;
我们可以通过着色器程序对象来动态设置这个颜色:
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUniform4f(vertexColorLocation, 1.0f, 0.0f, 0.0f, 1.0f);
glGetUniformLocation找到uniform变量的位置, glUniform指定当前程序对象的uniform变量的值。glGetUniformLocation返回-1就表示没有找到这个变量。glUniform函数名后面有分量数及类型,如glUniform4f就表示需要4个float作为它的值。
2.封装一个简易的着色器类
代码Github地址:https://github.com/gongjianbo/LearnTheOpenGL
整体比较简单,就是把着色器的初始化流程封装了下,以及对uniform变量操作的封装。教程是读取文件来初始化类的,我为了方便直接使用的字符串。
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <string>
class MyShader
{
public:
//根据着色器代码来初始化
MyShader(const std::string& vertexCode, const std::string& fragmentCode)
{
init(vertexCode, fragmentCode);
}
//激活程序
void useProgram()
{
glUseProgram(programID);
}
// uniform工具函数
void setInt(const std::string& name, int value)
{
glUniform1i(glGetUniformLocation(programID, name.c_str()), value);
}
void setFloat(const std::string& name, float value)
{
glUniform1f(glGetUniformLocation(programID, name.c_str()), value);
}
private:
void init(const std::string& vertexCode, const std::string& fragmentCode)
{
const char* vertex_shader_code = vertexCode.c_str();
const char* fragment_shader_code = fragmentCode.c_str();
//顶点着色器
unsigned int vertex_shader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex_shader, 1, &vertex_shader_code, NULL);
glCompileShader(vertex_shader);
checkError(vertex_shader, "vertex");
//片段着色器
unsigned int fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment_shader, 1, &fragment_shader_code, NULL);
glCompileShader(fragment_shader);
checkError(fragment_shader,"fragment");
//着色器程序
programID = glCreateProgram();
glAttachShader(programID, vertex_shader);
glAttachShader(programID, fragment_shader);
glLinkProgram(programID);
checkError(programID, "program");
// 删除着色器,它们已经链接到我们的程序中了
glDeleteShader(vertex_shader);
glDeleteShader(fragment_shader);
}
void checkError(unsigned int name,const std::string &type)
{
int check_flag;
char check_info[1024];
if (type != "program") {
glGetShaderiv(name, GL_COMPILE_STATUS, &check_flag);
if (!check_flag) {
glGetShaderInfoLog(name, 1024, NULL, check_info);
std::cout << type << " error:" << check_info << std::endl;
}
}
else {
glGetShaderiv(name, GL_LINK_STATUS, &check_flag);
if (!check_flag) {
glGetProgramInfoLog(name, 1024, NULL, check_info);
std::cout << type << " error:" << check_info << std::endl;
}
}
}
private:
unsigned int programID;
};
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
const std::string vertexShaderSource = R"(
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;
uniform float aSize;
out vec3 ourColor;
void main()
{
gl_Position = vec4(aPos*aSize, 1.0);
ourColor = aColor;
}
)";
//只有一个分量就不是vec2了,直接写float
const std::string fragmentShaderSource = R"(
#version 330 core
in vec3 ourColor;
out vec4 outColor;
void main()
{
outColor = vec4(ourColor, 1.0);
}
)";
int main()
{
// glfw: initialize and configure
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 window creation
GLFWwindow* window = glfwCreateWindow(800, 600, "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 all OpenGL function pointers
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
{
std::cout << "Failed to initialize GLAD" << std::endl;
glfwTerminate();
return -1;
}
int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;
// build and compile our shader program
MyShader a_shader(vertexShaderSource,fragmentShaderSource);
// 顶点数据
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);
glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
//glEnableVertexAttribArray(1); //卸载glVertexAttribPointer前后都可以
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
//渲染循环
while (!glfwWindowShouldClose(window))
{
processInput(window);
// render
glClear(GL_COLOR_BUFFER_BIT);
a_shader.useProgram();
glBindVertexArray(VAO);
float timeValue = (float)glfwGetTime();
float sizeValue = sin(timeValue) / 2.0f + 0.5f;
a_shader.setFloat("aSize", sizeValue);
// render the triangle
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glfwTerminate();
return 0;
}
void processInput(GLFWwindow* window)
{
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{
glViewport(0, 0, width, height);
}