着色器(Shader)是运行在GPU上的小程序。这些小程序为图形渲染管线的某个特定部分而运行。
从基本意义上来说,着色器只是一种把输入转化为输出的程序。
着色器结构
一个典型的着色器有下面的结构:
#version version_number
in type in_variable;
in type in_variable_2;
out type out_variable;
uniform type uniform_1;
int main()
{
// 处理输入并进行一些图形操作
...
// 输出处理过的结果到输出变量
out_variable_name = stuff_we_processed;
}
着色器的开头总是要声明版本,接着是输入和输出变量、uniform
和main
函数。每个着色器的入口点都是main
函数,在这个函数中我们处理所有的输入变量,并将结果输出到输出变量中。
uniform
是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform
和顶点属性有些不同。首先,uniform
是全局的,意味着uniform
变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform
值设置成什么,uniform
会一直保存它们的数据,直到它们被重置或更新。
可以在一个着色器中添加uniform
关键字至类型和变量名前来声明一个GLSL的uniform
。接着就可以在着色器中使用新声明的uniform
了。
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor;
void main()
{
FragColor = ourColor;
}
这里在片段着色器中声明了一个uniform
是vec4
类型的ourColor
,并把片段着色器的输出颜色设置为uniform值的内容。
因为uniform
是全局变量,可以在任何着色器中定义它们,而无需通过顶点着色器作为中介。顶点着色器中不需要这个uniform
,所以不用在那里定义它。
为了向这个uniform
中传递数据,需要在CPU程序中获取到该属性的索引,然后更新其值。
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, 0.5f, 0.0f, 1.0f);
通过glGetUniformLocation
可以查询uniform
的位置值。为查询函数提供着色器程序和uniform
的名字。如果glGetUniformLocation
返回-1
就代表没有找到这个位置值。
通过glUniform4f
函数可以设置vec4的float类型的uniform
值。类似的也有glUniform2i
对应vec2的int数组的uniform
等。注意,查询uniform
地址不要求你之前使用过着色器程序,但是更新一个uniform
之前你必须先使用程序(调用glUseProgram
),因为它是在当前激活的着色器程序中设置uniform
的。
向量
在顶点着色器中,每个输入变量也叫顶点属性(Vertex Attribute)。我们能声明的顶点属性是有上限的,它一般由硬件来决定。
OpenGL确保至少有16个包含4分量的顶点属性可用,但是有些硬件或许允许更多的顶点属性,可以通过glGetIntegerv
函数查询GL_MAX_VERTEX_ATTRIBS
来获取具体的上限。
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
GLSL中包含C等其它语言大部分的默认基础数据类型:int
、float
、double
、uint
和bool
,,以及两种容器类型向量(Vector)和矩阵(Matrix)。
GLSL中的向量是一个可以包含有2、3或者4个分量的容器,分量的类型可以是前面默认基础类型的任意一个。默认的vecn
表示包含n
个float
分量的默认向量,对应的有bvecn
表示包含n
个bool
分量的向量,ivecn
表示包含n
个int
分量的向量等。可以分别使用.x
、.y
、.z
和.w
来获取它们的第1、2、3、4个分量。
输入输出
GLSL定义了in
和out
关键字,每个着色器使用这两个关键字设定输入和输出,只要一个输出变量与下一个着色器阶段的输入匹配,它就会传递下去。所以,如果打算从一个着色器向另一个着色器发送数据,必须在发送方着色器中声明一个输出,在接收方着色器中声明一个类似的输入。当类型和名字都一样的时候,OpenGL就会把两个变量链接到一起,它们之间就能发送数据了(这是在链接程序对象时完成的)。
顶点着色器的输入特殊在,它从顶点数据中直接接收输入。为了定义顶点数据该如何管理,使用location
这一元数据指定输入变量,这样才可以在CPU上配置顶点属性。例如前面使用过的layout (location = 0)
,顶点着色器需要为它的输入提供一个额外的layout
标识。
而片段着色器需要一个vec4
颜色输出变量,因为片段着色器需要生成一个最终输出的颜色。如果在片段着色器没有定义输出颜色,OpenGL会把你的物体渲染为黑色(或白色)。
编写着色器类
有了上面的了解,目前已经可以成功地通过着色器来构建我们的图形了。但是每次更改都要改动一大堆代码,它们分布在整个程序中,每次相当麻烦。并且,目前的程序是以字符串形式写在程序里的,这显然是不符合需求的,应该改为从文件中读取。
基于这些需求,可以将着色器部分抽象成一个类,对外提供这些接口,从而便于管理和改动。
下面是着色器类的头文件
#pragma once
#include <string>
#include <iostream>
#include <GL/glew.h>
#include <fstream>
#include <sstream>
#include <unordered_map>
struct ShaderProgramSource
{
std::string VertexSource;
std::string FragmentSource;
};
class Shader
{
private:
unsigned int m_RendererID;
std::string m_file;
std::unordered_map<std::string, int> m_location_memory;
int GetUniformLocation(const std::string& name);
ShaderProgramSource ParseShader();
unsigned int compile_shader(const std::string& source, unsigned int type);
unsigned int create_shader(const std::string& vertexShader, const std::string& fragmentShader);
public:
Shader(const std::string& filename);
~Shader();
void Bind() const;
void Unbind() const;
// set uniforms
void SetUniform4f(const std::string& name, float v0, float v1, float v2, float v3);
void SetUniform1i(const std::string& name, int value);
void SetUniform1f(const std::string& name, float value);
};
首先着色器类需要储存着色器程序的ID,它的构造器需要顶点和片段着色器源代码的文件路径,这样我们就可以把源码的文本文件储存在硬盘上了。其次,为了方便调试,这里将文件名也作为变量储存了。
然后是如何读取和设置程序,这里将顶点着色器和片段着色器内容放在同一个文件中,因此需要一个额外的解析工作。其余部分与之前类似,将其放进函数中即可。为了方便起见,提供了设置Uniform的方法和绑定解绑的方法。
下面是具体的实现
#include "Shader.h"
#include "renderer.h"
int Shader::GetUniformLocation(const std::string& name)
{
if (m_location_memory.find(name) != m_location_memory.end()) {
return m_location_memory[name];
}
GLCall(int location = glGetUniformLocation(m_RendererID, name.c_str()));
m_location_memory[name] = location;
return location;
}
Shader::Shader(const std::string& filename):m_file(filename),m_RendererID (0)
{
GLCall(ShaderProgramSource source = ParseShader());
m_RendererID = create_shader(source.VertexSource, source.FragmentSource);
}
Shader::~Shader()
{
GLCall(glDeleteProgram(m_RendererID));
}
void Shader::Bind() const
{
GLCall(glUseProgram(m_RendererID));
}
void Shader::Unbind() const
{
GLCall(glUseProgram(0));
}
ShaderProgramSource Shader::ParseShader()
{
std::ifstream file(m_file);
enum class ShaderType
{
NONE = -1, VERTEX = 0, FRAGMENT = 1
};
std::stringstream ss[2];
std::string line;
ShaderType type = ShaderType::NONE;
while (getline(file, line)) {
if (line.find("#shader") != std::string::npos)
{
if (line.find("vertex") != std::string::npos)
{
type = ShaderType::VERTEX;
}
else if (line.find("fragment") != std::string::npos)
{
type = ShaderType::FRAGMENT;
}
}
else {
ss[(int)type] << line << "\n";
}
}
return { ss[0].str(),ss[1].str() };
}
void Shader::SetUniform4f(const std::string& name, float v0, float v1, float v2, float v3)
{
int location = GetUniformLocation(name);
ASSERT(location != -1);
GLCall(glUniform4f(location, v0, v1, v2, v3));
}
void Shader::SetUniform1i(const std::string& name, int value)
{
int location = GetUniformLocation(name);
ASSERT(location != -1);
GLCall(glUniform1i(location, value));
}
void Shader::SetUniform1f(const std::string& name, float value)
{
int location = GetUniformLocation(name);
ASSERT(location != -1);
GLCall(glUniform1f(location, value));
}
unsigned int Shader::compile_shader(const std::string& source, unsigned int type)
{
unsigned int id = glCreateShader(type);
const char* src = source.c_str();
glShaderSource(id, 1, &src, nullptr);
glCompileShader(id);
int result;
glGetShaderiv(id, GL_COMPILE_STATUS, &result);
if (result == GL_FALSE)
{
int length;
glGetShaderiv(id, GL_INFO_LOG_LENGTH, &length);
char* message = (char*)alloca(sizeof(char) * (length));
glGetShaderInfoLog(id, length, &length, message);
std::cout << (type == GL_VERTEX_SHADER ? "Vertex" : "") << " shader编译失败" << std::endl;
std::cout << message << std::endl;
glDeleteShader(id);
return 0;
}
return id;
}
unsigned int Shader::create_shader(const std::string& vertexShader, const std::string& fragmentShader)
{
unsigned int program = glCreateProgram();
unsigned int vs = compile_shader(vertexShader, GL_VERTEX_SHADER);
unsigned int fs = compile_shader(fragmentShader, GL_FRAGMENT_SHADER);
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
glValidateProgram(program);
glDeleteShader(vs);
glDeleteShader(fs);
return program;
}
这个renderer.h是自定义的一个头文件,中定义了一个错误检测的宏定义GLCall
,其功能就是如果某个函数运行出错就会打印错误码和行号。如果不使用的话,直接去掉即可。
参考资料
[1] https://learnopengl-cn.readthedocs.io/zh/latest/01%20Getting%20started/03%20Hello%20Window/
[2] http://bit.ly/2lt7ccM