着色器(shaders)
-
简单的理解,着色器就是将输入转换为输出的程序,同时着色器也是非常独立的程序,它们之间的通讯只能通过它们的输入和输出。
OpenGL着色器主要内容
1. GLSL
- 着色器典型的结构:
#version version_number
in type in_variable_name;
in type in_variable_name;
out type out_variable_name;
void main()
{
// 处理输入,做一些图形操作
// 将处理结果设置到输出变量
out_variable_name = werid_stuff_we_processed;
}
1. 声明版本(和指定渲染模式)。
2. 定义输入变量。
3. 定义输出变量。
4. 在main函数中设置输出变量的值。
- 当针对顶点着色器时,输入变量也称为顶点属性(vertex attribute)。我们可以声明的顶点属性的最大数量由硬件限制,但是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;
2. 类型
-
矢量(vector):在GLSL中矢量是一个由1,2,3或4个基本类型组件组成的容器。可以有如下的形式(其中n代表组件的数量):
-
vecn
:n个float的矢量(默认)。 -
bvecn
:n个boolean的矢量。 -
ivecn
:n个整数的矢量。 -
uvecn
:n个无符号整数的矢量。 -
dvecn
:n个double的矢量。
-
- 矢量通过
vec.x, vec.y, vec.z, vec.w
访问相应的组件。在GLSL中,颜色还可以通过rgba
访问,纹理(texture)可以通过stpq
访问。 - 矢量允许我们进行一些灵活的组件选择,叫做swizzling,语法类似下面的代码:
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);
3. 输入和输出
- 当一个输出变量与下一着色器阶段的输入变量匹配时,变量将在两个着色器间传递。
- 在输入输出方面,比较特殊的两个着色器是顶点着色器和片元着色器。
- 顶点着色器:直接从顶点数据接收输入,使用
location
元数据来指定输入变量以便我们可以在CPU侧配置顶点属性。
- 顶点着色器:直接从顶点数据接收输入,使用
- 片元着色器:因为片元着色器需要生成一个最终输出颜色,因此该着色器需要一个
vec4
的颜色输出变量。如果没有指定一个输出颜色,那么片元的颜色将是不确定的。
- 片元着色器:因为片元着色器需要生成一个最终输出颜色,因此该着色器需要一个
- 如果我们要在两个着色器之间传递变量,那么我们需要在发送的着色器中声明一个输出变量,在接收的着色器中声明一个匹配的输入变量。这样,在链接着色器程序时,两个着色器的变量会链接到一起。下面的着色器显示从顶点着色器传递颜色值到片元着色器。
// 顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos;
out vec4 vertexColor; // 指定一个颜色输出给片元着色器
void main()
{
gl_Position = vec4(aPos, 1.0);
vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 设置输出变量值 dark-red
}
// 片元着色器
#version 330 core
out vec4 FragColor;
in vec4 vertexColor; // 从顶点着色器获取值的输入变量(相同的名称和数据类型)
void main()
{
FragColor = vertexColor; // 设置输出变量值
}
-
将上述着色器应用到我们前一篇博客的代码中,运行效果如下:
着色器传递变量效果
4. Uniforms
- Uniforms是另一种从CPU侧,即我们的OpenGL程序传递数据到GPU中的着色器的方式。
- Uniforms与顶点属性的区别:
- Uniforms是全局的,意味着每个着色器程序对象中,uniform变量是唯一的,且可以从着色器程序中任何阶段的任何着色器进行访问。
- 不管你将uniforms值设置为什么,值会一直保持直到被重置或更新。
- 使用
uniform
关键字声明Uniform变量。
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 该变量的值在我们的OpenGL代码中设置
void main()
{
FragColor = ourColor; // 设置输出变量值
}
- 注意:如果你声明了一个uniform,但是没有在你的GLSL代码使用,那么编译器将会在编译版本中移除,这可能导致一些不确定的问题。
- 设置uniform变量的值
float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
1. glfwGetTime()函数获取程序运行时间,使用`sin()`函数转换为一个[0.0, 1.0]之间的颜色值。
2. glGetUniformLocation()函数查询uniform变量的位置(示例中为ourColor)。
3. 使用getUniform4f()函数设置uniform变量的值。
-
将上述片元着色器应用到我们前一篇博客的代码中,运行效果如下,矩形将在绿色和黑色之间变化:
Uniform设置效果图1
5. 更多属性(attributes)
- 为顶点数据添加颜色属性:
// 顶点数据
float vertices[] = {
// positions // colors
-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
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);
ourColor = aColor; // 将顶点数据输入的颜色设置为输出
}
- 调整片元着色器接收顶点着色器的颜色输出:
#version 330 core
out vec4 FragColor;
in vec3 ourColor;
void main()
{
FragColor = vec4(ourColor, 1.0f);
}
- 包含位置和颜色属性的VBO内存可能如下图所示:(图片取自书中)
VBO内存示意图 - 使用
glVertexAttribPointer
函数更新顶点数据的格式,参数的值设置对比上图的内存情形:
// 1. 设置顶点位置属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. 设置顶点颜色属性
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1);
-
运行结果:
添加颜色属性运行效果
6. 编写我们自己的着色器类
- 编写一个着色器类将顶点着色器和片元着色器的创建和编译,以及着色器程序的链接封装起来。
// 着色器头文件
#pragma once
#include <glad/glad.h>
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
class Shader
{
public:
// the program ID
unsigned int ID;
// constructor reads and builds the shader
Shader(const char* vertexPath, const char* fragmentPath);
// use/active the shader
void use();
// utility uniform functions
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;
};
// 着色器类实现文件
#include <iostream>
#include <fstream>
#include "shader.h"
Shader::Shader(const char* vertexPath, const char* fragmentPath)
{
//------1. 从指定路径读取顶点/片元着色器程序内容
std::string vertexCode;
std::string fragmentCode;
std::ifstream vShaderFile;
std::ifstream fShaderFile;
// ensure ifstream objects can throw exceptions
vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
try
{
// open file
vShaderFile.open(vertexPath);
fShaderFile.open(fragmentPath);
std::stringstream vShaderStream, fShaderStream;
// read file's buffer contents into stream
vShaderStream << vShaderFile.rdbuf();
fShaderStream << fShaderFile.rdbuf();
// close file handlers
vShaderFile.close();
fShaderFile.close();
// convert stream into string
vertexCode = vShaderStream.str();
fragmentCode = fShaderStream.str();
}
catch(std::ifstream::failure e)
{
std::cout << "EEROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
}
const char* vShaderCode = vertexCode.c_str();
const char* fShaderCode = fragmentCode.c_str();
//------2. 创建和编译着色器
unsigned int vertex, fragment;
int success;
char infoLog[512];
//---------2.1 创建和编译顶点着色器
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
// print compile errors if any
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertex, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}
//---------2.2 创建和编译片元着色器
fragment = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment, 1, &fShaderCode, NULL);
glCompileShader(fragment);
glGetShaderiv(fragment, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(fragment, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
}
//------3. 链接着色器程序
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::LINK_FAILED\n" << infoLog << std::endl;
}
//------4. 链接着色器程序后删除着色器对象
glDeleteShader(vertex);
glDeleteShader(fragment);
}
// 激活着色器程序
void Shader::use()
{
glUseProgram(ID);
}
// 一些辅助函数,用于设置unfirom的值
void Shader::setBool(const std::string& name, bool value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
void Shader::setInt(const std::string& name, int value) const
{
glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}
void Shader::setFloat(const std::string& name, float value) const
{
glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
- 着色器类的调用
// 声明着色器类(路径取决于shader文件的存放位置,文件后缀无关紧要)
Shader ourShader("./shaders/VertexShader.vs", "./shaders/FragmentShader.fs");
// 在渲染循环中调用
ourShader.use();
ourShader.setFloat("ourColor", 0.2f);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
网友评论