SoftGLRender源码:着色器ShaderGLSL、ProgramGLSL
介绍
文件:GLSLUtils.h
, GLSLUtils.cpp
GLSLUtils
模块包括2个类:ShaderGLSL
,ProgramGLSL
. 提供对GLSL着色器程序的封装.
ShaderGLSL
主要用于管理着色器(Shader);
ProgramGLSL
主要用于管理着色器程序(Shader Program).
着色器
着色器是一个用GLSL编写的小程序,用于GPU上执行特定的渲染计算. 主要有这几种类型:
- 顶点着色器(Vertex Shader)
- 片元着色器(Fragment Shader)
- 其他着色器,如几何着色器、计算着色器等
着色器必须编译后才能使用,不能单独使用.
典型的顶点着色器(Vertex Shader):
#version 330 core
layout(location = 0) in vec3 position;
void main() {
gl_Position = vec4(position, 1.0);
}
着色器的基本结构
组成部分 | 说明 | 示例 |
---|---|---|
#version |
指定 GLSL 版本(必须第一行) | #version 330 core |
输入变量 | 用于接收数据 | layout(location = 0) in vec3 aPos; |
输出变量 | 输出给下一阶段 | out vec3 FragColor; |
Uniforms(统一变量) | 传入矩阵、颜色等不变数据 | uniform mat4 model; |
Varyings(传递变量) | 顶点到片段阶段传值 | out vec3 vColor; |
主函数 | 必须有 void main() |
void main() { ... } |
着色器使用流程
从OpenGL函数调用角度看,典型的着色器使用流程:
[GLSL代码](.glsl文件) ──> glCreateShader ──> glShaderSource ──> glCompileShader
↓
glCreateProgram ──> glAttachShader ──> glLinkProgram ──> glUseProgram
↓
glDrawArrays / glDrawElements
ShaderGLSL类(管理着色器)
ShaderGLSL
本质是管理一个着色器的类,而着色器本身是一段GLSL文本程序. 因此,ShaderGLSL
需要管理着色器文本,包括:
- 读取着色器文件;
- 添加GLSL 的预处理指令;
- 将着色器转换为兼容版本
ShaderGLSL声明
constexpr char const* OpenGL_GLSL_VERSION = "#version 330 core";
// 管理着色器对象(Shader)
class ShaderGLSL {
public:
explicit ShaderGLSL(GLenum type) : type_(type) {
header_ = OpenGL_GLSL_VERSION;
header_ += "\n";
}
~ShaderGLSL() { destroy(); }
void setHeader(const std::string& header);
void addDefines(const std::string& def);
bool loadSource(const std::string& source);
bool loadFile(const std::string& path);
void destroy();
// id_为0,意味着创建着色器对象失败
inline bool empty() const { return 0 == id_; }
inline GLuint getId() const { return id_; }
private:
static std::string compatibleVertexPreprocess(const std::string& source);
static std::string compatibleFragmentPreprocess(const std::string& source);
private:
GLenum type_; // 着色器类型 GL_VERTEX_SHADER, GL_FRAGMENT_SHADER
GLuint id_ = 0; // 着色器id
std::string header_; // 头部信息
std::string defines_; // 宏定义
};
数据成员:
type_
着色器类型,如GL_VERTEX_SHADER
,GL_FRAGMENT_SHADER
id_
着色器id,由glCreateShader
生成header_
存放着色器代码的头部信息,通常包括:- GLSL版本声明:如
#version 330 core
or#version 330 es
- 一些全局声明
- 用于多个着色器之间共享的统一头部信息
- GLSL版本声明:如
defines_
存放宏定义(#define),通常包括:- 条件编译
- 支持配置灵活的shader功能组合
核心函数:
setHeader
/addDefines
插入版本头信息 / 自定义宏loadFile
从磁盘文件加载GLSL源码loadSource
从字符串加载GLSL源码destroy
销毁释放Shader资源
重要但非必须功能:
compatibleVertexPreprocess
对顶点着色器代码预处理,使其与特定版本OpenGL或特定环境兼容compatibleFragmentPreprocess
对片元着色器代码预处理,类似于compatibleVertexPreprocess,但额外处理了in变量的location布局限定符
ShaderGLSL实现
构造与析构
构造函数中,只是简单的添加了头部信息(header_
),并调用没有glCreateShader
创建Shader,这一步延迟到了loadSource
加载GLSL源码.
explicit ShaderGLSL(GLenum type) : type_(type) {
header_ = OpenGL_GLSL_VERSION;
header_ += "\n";
}
~ShaderGLSL() { destroy(); }
void ShaderGLSL::destroy() {
if (id_) {
// 删除着色器
GL_CHECK(glDeleteShader(id_));
id_ = 0;
}
}
type_
着色器类型(顶点 or 片元),在ShaderGLSL
构造时已经决定,后续无法随意更改.
glDeleteShader
只能删除由glCreateShader
创建的着色器对象,会被标记为待删除对象;着色器(Shader)只有不再被附加(glAttachShader
)到任何程序对象(Shader Program)上时,才会被真正删除.
要让着色器从程序分离,可调用glDetachShader
添加头信息、宏定义
这部分代码简单. 注意header_
与defines_
的区别:
header_
存放GLSL源码头部信息;defines_
存放宏定义.
void ShaderGLSL::setHeader(const std::string& header) {
header_ = header;
}
void ShaderGLSL::addDefines(const std::string& def) {
defines_ = def;
}
加载着色器源码
// 从字符串加载GLSL源码
bool ShaderGLSL::loadSource(const std::string& source) {
id_ = glCreateShader(type_);
std::string shaderStr;
if (type_ == GL_VERTEX_SHADER) { // 顶点着色器
shaderStr = compatibleVertexPreprocess(header_ + defines_ + source);
}
else if (type_ == GL_FRAGMENT_SHADER) { // 片元着色器
shaderStr = compatibleFragmentPreprocess(header_ + defines_ + source);
}
if (shaderStr.empty()) {
LOGE("ShaderGLSL::loadSource failed: empty source");
return false;
}
const char* shaderStrPtr = shaderStr.c_str();
auto length = (GLint)shaderStr.length();
GL_CHECK(glShaderSource(id_, 1, &shaderStrPtr, &length)); // GLSL源码加载到着色器对象
GL_CHECK(glCompileShader(id_)); // 编译着色器程序
GLint isCompiled = 0;
GL_CHECK(glGetShaderiv(id_, GL_COMPILE_STATUS, &isCompiled));
if (isCompiled == GL_FALSE) {
GLint maxLength = 0;
GL_CHECK(glGetShaderiv(id_, GL_INFO_LOG_LENGTH, &maxLength));
std::vector<GLchar> infoLog(maxLength);
GL_CHECK(glGetShaderInfoLog(id_, maxLength, &maxLength, &infoLog[0])); // 获取着色器的编译错误log
LOGE("compile shader failed: %s", &infoLog[0]);
destroy(); // 回收资源
return false;
}
return true;
}
// 从磁盘文件加载GLSL源码
bool ShaderGLSL::loadFile(const std::string& path) {
std::string source = FileUtils::readText(path);
if (source.length() <= 0) {
LOGE("read shader source failed");
return false;
}
return loadSource(source);
}
loadSource
从字符串加载GLSL源码,支持顶点着色器和片元着色器. 具体来说,包括插入头部信息、宏定义,利用OpenGL函数创建shader并加载GLSL源码
-
异常处理:
- GLSL源码为空,打印错误log;
- 编译失败,打印编译log;
-
glCreateShader
创建着色器对象. 如果返回值为0,意味着创建失败 -
glShaderSource
将GLSL源码加载到一个着色器对象 -
glCompileShader
编译指定着色器对象 -
glGetShaderiv(id_, GL_INFO_LOG_LENGTH, &maxLength)
检查着色器是否编译成功
glGetShaderiv
常见查询常量:
查询常量 | 说明 |
---|---|
GL_COMPILE_STATUS |
是否编译成功 |
GL_SHADER_TYPE |
着色器类型(如 GL_VERTEX_SHADER) |
GL_DELETE_STATUS |
是否被标记为删除 |
GL_INFO_LOG_LENGTH |
编译信息日志的长度 |
GL_SHADER_SOURCE_LENGTH |
源代码长度 |
loadFile
从磁盘文件加载GLSL源码. 由于GLSL源码文件通常较小(<100K),因此程序直接将其加载到内存,存储为std::string
,再用loadSource
加载.
预处理
顶点着色器源码预处理
compatibleVertexPreprocess
对顶点着色器代码预处理,使其兼容不同版本OpenGL环境或驱动.
因为不同OpenGL版本、驱动或者平台,对着色器语法支持不尽相同,该函数利用正则表达式对GLSL源码修改,以解决兼容性问题.
// 对着色器代码预处理,使其与特定版本OpenGL或特定环境兼容
std::string ShaderGLSL::compatibleVertexPreprocess(const std::string& source) {
// 字符串前加R: 原始字符串字面量(Raw String Literal),字符串保持“原始”形式,避免转义干扰
std::regex outLocationRegex(R"(layout\s*\(\s*location\s*=\s*\d+\s*\)\s*out\s*)");
std::regex std140Regex(R"(layout\s*\(.*std140.*\)\s*uniform\s*)");
std::regex uniformBindingRegex(R"(layout\s*\(.*binding\s*=.*\)\s*uniform\s*)");
// 1. 移除输出变量的location限定
// 将类似 layout(location = 0) out语法替换为out, 移除显式的location指定
std::string result = std::regex_replace(source, outLocationRegex, "out ");
// 2. 标准化std140布局声明
// 将各种形式的std140布局声明(可能带空格或其他参数)统一标准化为layout (std140) uniform
result = std::regex_replace(result, std140Regex, "layout (std140) uniform ");
// 3. 移除uniform 绑定的声明
// 将layout(binding = 0) uniform 语法替换为uniform
result = std::regex_replace(result, uniformBindingRegex, "uniform ");
return result;
}
- 移除
layout(location = X)
的输出变量限定
因为有些OpenGL ES/WebGL不支持layout(location =...)
std::regex outLocationRegex(R"(layout\s*\(\s*location\s*=\s*\d+\s*\)\s*out\s*)");
result = std::regex_replace(source, outLocationRegex, "out ");
例如,
输入:layout(location = 0) out vec3 vColor;
输出:out vec3 vColor;
- 统一std140布局语法
因为有些驱动只支持严格语法格式.
std::regex std140Regex(R"(layout\s*\(.*std140.*\)\s*uniform\s*)");
result = std::regex_replace(result, std140Regex, "layout (std140) uniform ");
例如,
输入:layout (binding = 1, std140) uniform Matrices
输出:layout (std140) uniform Matrices
GLSL std140布局可参见:GLSL std140布局规则 CSDN
- 移除
binding = ...
的uniform绑定
因为某些平台如WebGL不支持
std::regex uniformBindingRegex(R"(layout\s*\(.*binding\s*=.*\)\s*uniform\s*)");
result = std::regex_replace(result, uniformBindingRegex, "uniform ");
例如,
输入:layout (binding = 2) uniform LightData
输出:uniform LightData
片元着色器源码预处理
compatibleFragmentPreprocess
类似于顶点着色器源码预处理,对片元着色器源码预处理,使其兼容不同版本OpenGL环境或驱动,但额外处理了in变量的location布局限定符.
std::string ShaderGLSL::compatibleFragmentPreprocess(const std::string& source) {
std::regex inLocationRegex(R"(layout\s*\(\s*location\s*=\s*\d+\s*\)\s*in\s*)");
std::regex outLocationRegex(R"(layout\s*\(\s*location\s*=\s*\d+\s*\)\s*out\s*)");
std::regex std140Regex(R"(layout\s*\(.*std140.*\)\s*uniform\s*)");
std::regex uniformBindingRegex(R"(layout\s*\(.*binding\s*=.*\)\s*uniform\s*)");
// 1. 移除输入变量in的location限定符
// 将类似 layout(location = 0) in vec3 color; 替换为 in vec3 color;,移除显式的 location 指定
std::string result = std::regex_replace(source, inLocationRegex, "in ");
// 2. 移除输出变量(out)的 location 限定符
// 将类似 layout(location = 0) out vec4 fragColor; 替换为 out vec4 fragColor
result = std::regex_replace(result, outLocationRegex, "out ");
// 3. 标准化 std140 布局的 Uniform 块声明
// 将各种形式的 std140 布局声明(如 layout(std140, binding=0) uniform)统一标准化为 layout (std140) uniform
result = std::regex_replace(result, std140Regex, "layout (std140) uniform ");
// 4. 移除 Uniform 的 binding 绑定声明
// 将layout(binding = 1) uniform sampler2D tex; 替换为 uniform sampler2D tex;,移除显式的 binding 指定
result = std::regex_replace(result, uniformBindingRegex, "uniform ");
return result;
}
- 移除
layout(location = X)
输入变量
std::regex inLocationRegex(R"(layout\s*\(\s*location\s*=\s*\d+\s*\)\s*in\s*)");
result = std::regex_replace(source, inLocationRegex, "in ");
例如,
输入:layout(location = 1) in vec3 normal;
输出:in vec3 normal;
其他,同顶点着色器的预处理,不再详述.
着色器程序
着色器程序是多个着色器的综合体,通过OpenGL将它们链接组成一个完成的程序,用于控制整个图形渲染管线的一段,如从顶点到片元.
着色器程序使用流程
典型的着色器程序创建流程:
- 编译着色器(
glCompileShader
) - 创建一个程序对象(
glCreateProgram
) - 将着色器附加到程序上(
glAttachShader
) - 链接程序(
glLinkProgram
) - 激活着色器程序(
glUseProgram
)
典型代码如下:
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, ...);
glCompileShader(vertexShader);
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, ...);
glCompileShader(fragmentShader);
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glUseProgram(shaderProgram); // 启用程序
ProgramGLSL类(管理着色器程序)
constexpr char const* OpenGL_GLSL_DEFINE = "OpenGL";
// 管理着色器程序(Shader Program)
class ProgramGLSL {
public:
ProgramGLSL() { addDefine(OpenGL_GLSL_DEFINE); }
~ProgramGLSL() { destroy(); }
void addDefine(const std::string& def);
bool loadSource(const std::string& vsSource, const std::string& fsSource);
bool loadFile(const std::string& vsPath, const std::string& fsPath);
void use() const;
void destroy();
inline bool empty() const { return 0 == id_; }
inline GLuint getId() const { return id_; }
private:
bool loadShader(ShaderGLSL& vs, ShaderGLSL& fs);
private:
GLuint id_ = 0;
std::string defines_;
};
构造与析构
构造函数将添加的默认的宏定义保存到defines_
,待后面加载着色器时添加到着色器源码中.
同样地,并没有在构造函数中创建着色器程序(glCreateProgram
),而是延迟到loadShader
加载着色器中.
析构函数销毁着色器程序(glDeleteProgram
)
ProgramGLSL() { addDefine(OpenGL_GLSL_DEFINE); }
~ProgramGLSL() { destroy(); }
// GLSL源码中的宏定义,形如 #define OpenGL
void ProgramGLSL::addDefine(const std::string& def) {
if (def.empty()) {
return;
}
defines_ += ("#define " + def + " \n");
}
void ProgramGLSL::destroy() {
if (id_) {
GL_CHECK(glDeleteProgram(id_));
id_ = 0;
}
加载着色器
ShaderGLSL中也有加载着色器,跟ProgramGLSL加载着色器什么区别?
ProgramGLSL
(着色器程序)利用ShaderGLSL
(着色器)加载着色器源码,一个着色器程序通常附加了多个着色器,至少包括顶点着色器、片元着色器.
loadSource
从字符串加载顶点着色器源码、片元着色器源码
bool ProgramGLSL::loadSource(const std::string& vsSource, const std::string& fsSource) {
ShaderGLSL vs(GL_VERTEX_SHADER); // 顶点着色器对象
ShaderGLSL fs(GL_FRAGMENT_SHADER); // 片元着色器对象
// 添加宏定义
vs.addDefines(defines_);
fs.addDefines(defines_);
if (!vs.loadSource(vsSource)) { // 加载顶点着色器GLSL源码
LOGE("load vertex shader source failed");
return false;
}
if (!fs.loadSource(fsSource)) { // 加载片元着色器GLSL源码
LOGE("load fragment shader source failed");
return false;
}
return loadShader(vs, fs);
}
创建着色器程序
加载完着色器后,需要调用loadShader
创建着色器程序.
主要工作:
glCreateProgram
创建着色器程序glAttachShader
将顶点着色器、片元着色器附加到着色器程序glLinkProgram
链接着色器程序glValidateProgram
验证已链接着色器程序是否可用glGetProgramiv
检查链接是否成功- 链接失败异常处理
bool ProgramGLSL::loadShader(ShaderGLSL& vs, ShaderGLSL& fs) {
id_ = glCreateProgram();
GL_CHECK(glAttachShader(id_, vs.getId())); // 将着色器对象附加到着色器程序
GL_CHECK(glAttachShader(id_, fs.getId()));
GL_CHECK(glLinkProgram(id_)); // 链接着色器程序
// 验证着色器程序在当前OpenGL状态下是否可执行,
// 包括检查着色器间的匹配性、资源绑定情况等
GL_CHECK(glValidateProgram(id_));
GLint isLinked = 0;
GL_CHECK(glGetProgramiv(id_, GL_LINK_STATUS, (int*)&isLinked)); // 检查链接是否成功
if (isLinked == GL_FALSE) { // 链接失败异常处理:打印错误log到stderr
GLint maxLength = 0;
GL_CHECK(glGetProgramiv(id_, GL_INFO_LOG_LENGTH, &maxLength));
std::vector<GLchar> infoLog(maxLength);
GL_CHECK(glGetProgramInfoLog(id_, maxLength, &maxLength, &infoLog[0]));
LOGE("link program failed: %s", &infoLog[0]);
destroy(); // 回收资源
return false;
}
return true;
}
激活着色器程序
ProgramGLSL
类本质是对着色器程序的包装,ProgramGLSL::use()
是对glUseProgram
的包装.
glUseProgram
用于激活着色器程序,渲染时调用. 也就是告诉GPU:现在开始,所有绘制操作都使用这个着色器程序.
void ProgramGLSL::use() const {
if (id_) {
// glUseProgram: 激活着色器程序, 渲染时调用
GL_CHECK(glUseProgram(id_));
}
else {
LOGE("failed to use program, not ready");
}
}