SoftGLRender源码:着色器ShaderGLSL、ProgramGLSL

介绍

文件:GLSLUtils.h, GLSLUtils.cpp

GLSLUtils模块包括2个类:ShaderGLSLProgramGLSL. 提供对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_SHADERGL_FRAGMENT_SHADER
  • id_ 着色器id,由glCreateShader生成
  • header_ 存放着色器代码的头部信息,通常包括:
    • GLSL版本声明:如#version 330 core or #version 330 es
    • 一些全局声明
    • 用于多个着色器之间共享的统一头部信息
  • 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;
}
  1. 移除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;

  1. 统一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

  1. 移除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;
}
  1. 移除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将它们链接组成一个完成的程序,用于控制整个图形渲染管线的一段,如从顶点到片元.

着色器程序使用流程

典型的着色器程序创建流程:

  1. 编译着色器(glCompileShader
  2. 创建一个程序对象(glCreateProgram
  3. 将着色器附加到程序上(glAttachShader
  4. 链接程序(glLinkProgram
  5. 激活着色器程序(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");
	}
}
posted @ 2025-05-20 00:35  明明1109  阅读(62)  评论(0)    收藏  举报