Ap1-1 运行期编译GLSL
你是否已经厌倦了每次写完GLSL着色器代码,都得在文件浏览器的地址栏中复制入glslc.exe路径 着色器文件路径 -c
这样的语句?
你可以用一些简单的C++代码来写个exe放桌面上,使得着色器拖上去就能编译:
#include <iostream> #include <sstream> #define COMPILER "glslc.exe路径" int main(int argc, char** argv) { //argv[0]是程序本身的路径,argv[1]开始是拖上去的文件的路径 for (int count = 1; count < argc; count++) { //显示被编译的文件名 std::cout << "Compile file: " << argv[count] << std::endl; //构造命令行语句 std::stringstream command; command << COMPILER << ' ' << argv[count] << " -c" //使用system(...)来执行命令行语句 system(command.str().c_str()); } system("pause"); return 0; }
不过,如果还在不断修改着色器代码,每次改完想看看效果前,都得从文件夹里把文件拖到编译用的exe上,那还是挺麻烦的。
而且,也可能会有需要根据具体情况,动态地生成着色器代码,然后再编译的需求。
出于以上考虑,这一节对如何在运行期编译GLSL进行简单介绍。
shaderc
Vulkan SDK中包含了谷歌的shaderc库,它提供将GLSL或HLSL代码编译到SPIR-V的功能。
所需的头文件
从Vulkan SDK的安装目录下找到Include/shaderc,里面是所需的头文件,将该文件夹复制到Vulkan相关文件的附加包含目录中。
所需的lib
Vulkan SDK的安装目录中,Lib文件夹下有这三个与shaderc相关的预编译库:
shaderc_combined.lib |
包含C运行库以外的所有依赖项 |
shaderc.lib |
只包含shaderc,需要其他依赖项,用起来比较麻烦,无视它 |
shaderc_shared.lib |
存根库,依赖项皆为动态链接 |
-
Vulkan SDK中提供的这个shaderc_combined.lib几乎是个静态库,但是它不包含C标准库中一些数学函数的实现,编译时要求动态链接。
如果需要静态链接,你可以从谷歌的Github仓库把项目拖下来自己编译成lib。
将shaderc_combined.lib和shaderc_shared.lib复制到Vulkan相关文件的附加库目录中。
然后新建一个头文件,加入以下代码:
#include "EasyVKStart.h" //意在使用<iostream>、<ifstream>、<span> #include <shaderc/shaderc.hpp> #ifdef NDEBUG #pragma comment(lib, "shaderc_combined.lib") #else #pragma comment(lib, "shaderc_shared.lib") #endif
-
Vulkan SDK中提供的shaderc_combined.lib不支持Debug Build,只在Release Build下链接它。
fCompileGlslToSpv
定义一个仿函数类fCompileGlslToSpv,并加入以下成员变量:
class fCompileGlslToSpv { shaderc::Compiler compiler; shaderc::CompileOptions options; shaderc::SpvCompilationResult result; /*待填充*/ };
-
shaderc::Compiler是shaderc.hpp中提供的对编译器的封装类型。
-
shaderc::CompileOptions是对编译选项的封装类型。
-
shaderc::SpvCompilationResult是用于接受编译结果的类型。
用Compiler的成员函数CompileGlslToSpv(...)将GLSL编译到SPIR-V:
SpvCompilationResult Compiler::CompileGlslToSpv(...) 的参数说明 |
|
---|---|
const char* source_text |
指向GLSL代码 |
size_t source_text_size |
GLSL代码的大小,单位是字节 |
shaderc_shader_kind shader_kind |
着色器类型,用shaderc_glsl_infer_from_source表示根据代码中的 |
const char* input_file_name |
代码文件的路径,见后文 |
const char* entry_point_name |
接入点函数名称 |
const CompileOptions& options |
编译选项 |
-
该函数不负责读取文件,但是要向input_file_name提供代码文件路径,之后需要从该路径出发,找到被#include的文件。
若被编译的代码中没有#include,则仅将input_file_name用于报错信息(随便填也没关系)。
因为Compiler::CompileGlslToSpv(...)不负责读取代码文件(其所有重载都没有读取文件的功能),定义一个fCompileGlslToSpv的静态成员函数来读取文件:
class fCompileGlslToSpv { shaderc::Compiler compiler; shaderc::CompileOptions options; shaderc::SpvCompilationResult result; //Static Function static void LoadFile(const char* filepath, std::vector<char>& binaries) { std::ifstream file(filepath, std::ios::ate | std::ios::binary); if (!file) { outStream << std::format("[ fCompileGlslToSpv ] ERROR\nFailed to open the file: {}\n", filepath); return; } size_t fileSize = size_t(file.tellg()); binaries.resize(fileSize); file.seekg(0); file.read(reinterpret_cast<char*>(binaries.data()), fileSize); file.close(); } };
然后来指定编译选项,就在fCompileGlslToSpv的构造函数中指定吧,暂且先只指定优化等级:
class fCompileGlslToSpv { shaderc::Compiler compiler; shaderc::CompileOptions options; shaderc::SpvCompilationResult result; //Static Function static void LoadFile(const char* filepath, std::vector<char>& binaries) { /*...*/ } public: fCompileGlslToSpv() { options.SetOptimizationLevel(shaderc_optimization_level_performance); /*待后续填充*/ } };
-
优化等级的可选项有:
shaderc_optimization_level_zero(无优化)
shaderc_optimization_level_size(优化程序大小)
shaderc_optimization_level_performance(优化程序效率)。
定义用于执行编译的成员函数,如类型名fCompileGlslToSpv中前缀的f所言,我打算让其实例可以像函数一般被使用:
class fCompileGlslToSpv { shaderc::Compiler compiler; shaderc::CompileOptions options; shaderc::SpvCompilationResult result; //Static Function static void LoadFile(const char* filepath, std::vector<char>& binaries) { /*...*/ } public: fCompileGlslToSpv() { options.SetOptimizationLevel(shaderc_optimization_level_performance); /*待后续填充*/ } //Non-const Function std::span<const uint32_t> operator()(std::span<const char> code, const char* filepath, const char* entry = "main") { result = compiler.CompileGlslToSpv(code.data(), code.size(), shaderc_glsl_infer_from_source, filepath, entry, options); outStream << result.GetErrorMessage(); //输出错误信息,没有错误的话不会输出任何东西 return { result.begin(), size_t(result.end() - result.begin()) * 4 }; } std::span<const uint32_t> operator()(const char* filepath, const char* entry = "main") { std::vector<char> binaries; LoadFile(filepath, binaries); if (size_t fileSize = binaries.size()) return (*this)(binaries, filepath, entry); return {}; } };
includer
Compiler::CompileGlslToSpv(...)不负责读取代码文件,当然也包括被#include的文件。
shaderc.hpp中定义了抽象类shaderc::CompileOptions::IncluderInterface作为读取被包含文件的接口类,定义fCompileGlslToSpv的内部类型includer以实现接口:
class fCompileGlslToSpv { struct includer: public shaderc::CompileOptions::IncluderInterface { /*待填充*/ }; //-------------------- shaderc::Compiler compiler; shaderc::CompileOptions options; shaderc::SpvCompilationResult result; //Static Function static void LoadFile(const char* filepath, std::vector<char>& binaries) { /*...*/ } public: fCompileGlslToSpv() { options.SetOptimizationLevel(shaderc_optimization_level_performance); /*待后续填充*/ } //Non-const Function std::span<const uint32_t> operator()(std::span<const char> code, const char* filepath, const char* entry = "main") { /*...*/ } std::span<const uint32_t> operator()(const char* filepath, const char* entry = "main") { /*...*/ } };
来看一下shaderc::CompileOptions::IncluderInterface的内容:
class IncluderInterface { public: virtual shaderc_include_result* GetInclude(const char* requested_source, shaderc_include_result type, const char* requesting_source, size_t include_depth) = 0; virtual void ReleaseInclude(shaderc_include_result* data) = 0; virtual ~IncluderInterface() = default; };
-
编译过程中遇到#include时,会调用GetInclude(...)读取文件,之后会调用ReleaseInclude(...)释放相关的动态分配内存。
shaderc_include_result* IncluderInterface::GetInclude(...) 的参数说明 |
|
---|---|
const char* requested_source |
着色器代码中被包含文件的路径( |
shaderc_include_result type |
shaderc_include_type_relative对应"requested_source", |
const char* requesting_source |
当前(包含 |
size_t include_depth |
包含的纵深,若最初通过Compiler::CompileGlslToSpv(...)编译的文件包含了文件A,A又包含了另一个文件B,则B的include_depth为2 |
-
"requested_source"和<requested_source>有什么差别由你自己定,我接下来的代码不区分两者,一概当做相对路径。
struct shaderc_include_result 的成员说明 |
|
---|---|
const char* source_name |
被包含文件的路径 |
size_t* source_name_length |
被包含文件的路径的长度,单位是字节 |
const char* content_name |
指向被包含文件的GLSL代码 |
size_t* content_length |
被包含文件的GLSL代码的大小,单位是字节 |
void* user_data |
填this指针即可,解释略 |
于是来实现GetInclude(...)和ReleaseInclude(...):
struct includer: public shaderc::CompileOptions::IncluderInterface { struct result_t : shaderc_include_result { std::string filepath; //用来存被包含文件的文件路径 std::vector<char> code; //用来存被包含文件的内容 }; shaderc_include_result* GetInclude(const char* requested_source, shaderc_include_result, const char* requesting_source, size_t) override { //↑无实参名的形参在我接下来的逻辑里都用不着 auto& result = *(new result_t); auto& filepath = result.filepath; auto& code = result.code; /*待填充*/ return &result; } void ReleaseInclude(shaderc_include_result* data) override { //将指针类型转到result_t*,以正确调用string和vector的析构器 delete static_cast<result_t*>(data); } };
获取当前GLSL着色器代码文件的路径:
filepath = requesting_source;
requested_source是被包含文件相对于requesting_source的路径(一般没人会自找麻烦用绝对路径,就不处理绝对路径了),那么将requesting_source尾部的文件名换成requested_source即是LoadFile(...)所需的被包含文件的路径:
filepath = requesting_source; size_t pos = filepath.rfind('/'); if (pos == -1) pos = filepath.rfind('\\'); filepath.replace(pos + 1 + filepath.begin(), filepath.end(), requested_source); LoadFile(filepath.c_str(), code);
-
std::string::rfind(...)从字符串尾部开始查找字符或子串,找不到时返回std::string::npos,值为size_t的最大值,C++中
无符号整形最大值 == -1
返回true。 -
根据个人习惯的不同,文件路径中的分隔符可能是斜杠也可能是反斜杠,这里反斜杠的写法为转义字符。
-
查找不到分隔符时,说明被包含文件和当前文件在同一文件夹中,
pos + 1
为0,替换整个字符串。 -
至少对于Windows的文件系统而言,"shader/../shader/FirstTriangle.frag.shader"与"shader/FirstTriangle.frag.shader"是等价的,因此不必对用于返回上级目录的"../"特地做处理。
填写result开头的shaderc_include_result部分,整个includer::GetInclude(...)如下:
shaderc_include_result* GetInclude(const char* requested_source, shaderc_include_result, const char* requesting_source, size_t) override { auto& result = *(new result_t); auto& filepath = result.filepath; auto& code = result.code; filepath = requesting_source; size_t pos = filepath.rfind('/'); if (pos == -1) pos = filepath.rfind('\\'); filepath.replace(pos + 1 + filepath.begin(), filepath.end(), requested_source); LoadFile(filepath.c_str(), code); static_cast<shaderc_include_result&>(result) = { filepath.c_str(), filepath.length(), code.data(), code.size(), this }; return &result; }
最后将includer设置到编译选项:
fCompileGlslToSpv() { options.SetOptimizationLevel(shaderc_optimization_level_performance); options.SetIncluder(std::make_unique<includer>()); }
-
CompileOptions::SetIncluder(...)要求的参数类型为std::unique_ptr<IncluderInterface>&&,即需要转移对象的所有权,由options管理IncluderInterface对象的生存期。
代码汇总及使用
这一节的代码没有上传到Github,整个文件的内容就写在这儿了:
#include "EasyVKStart.h" #include <shaderc/shaderc.hpp> #ifdef NDEBUG #pragma comment(lib, "shaderc_combined.lib") #else #pragma comment(lib, "shaderc_shared.lib") #endif class fCompileGlslToSpv { struct includer: public shaderc::CompileOptions::IncluderInterface { struct result_t : shaderc_include_result { std::string filepath; std::vector<char> code; }; shaderc_include_result* GetInclude(const char* requested_source, shaderc_include_result, const char* requesting_source, size_t) override { auto& result = *(new result_t); auto& filepath = result.filepath; auto& code = result.code; filepath = requesting_source; size_t pos = filepath.rfind('/'); if (pos == -1) pos = filepath.rfind('\\'); filepath.replace(pos + 1 + filepath.begin(), filepath.end(), requested_source); LoadFile(filepath.c_str(), code); static_cast<shaderc_include_result&>(result) = { filepath.c_str(), filepath.length(), code.data(), code.size(), this }; return &result; } void ReleaseInclude(shaderc_include_result* data) override { delete static_cast<result_t*>(data); } }; //-------------------- shaderc::Compiler compiler; shaderc::CompileOptions options; shaderc::SpvCompilationResult result; //Static Function static void LoadFile(const char* filepath, std::vector<char>& binaries) { std::ifstream file(filepath, std::ios::ate | std::ios::binary); if (!file) { outStream << std::format("[ fCompileGlslToSpv ] ERROR\nFailed to open the file: {}\n", filepath); return; } size_t fileSize = size_t(file.tellg()); binaries.resize(fileSize); file.seekg(0); file.read(reinterpret_cast<char*>(binaries.data()), fileSize); file.close(); } public: fCompileGlslToSpv() { options.SetOptimizationLevel(shaderc_optimization_level_performance); options.SetIncluder(std::make_unique<includer>()); } //Non-const Function std::span<const uint32_t> operator()(std::span<const char> code, const char* filepath, const char* entry = "main") { result = compiler.CompileGlslToSpv(code.data(), code.size(), shaderc_glsl_infer_from_source, filepath, entry, options); outStream << result.GetErrorMessage(); return { result.begin(), size_t(result.end() - result.begin()) * 4 }; } std::span<const uint32_t> operator()(const char* filepath, const char* entry = "main") { std::vector<char> binaries; LoadFile(filepath, binaries); if (size_t fileSize = binaries.size()) return (*this)(binaries, filepath, entry); return {}; } };
使用fCompileGlslToSpv的方法很简单,类似这样:
inline shaderModule CreateShaderModuleFromGlsl(fCompileGlslToSpv& fCompile, const char* filepath, const char* entry = "main") { auto code = fCompile(filepath, entry); return shaderModule(code.size(), code.data()); //必定发生RVO(返回值优化) } void CreatePipeline() { fCompileGlslToSpv fCompile; shaderModule vert = CreateShaderModuleFromGlsl(fCompile, "shader/FirstTriangle.vert.shader"); shaderModule frag = CreateShaderModuleFromGlsl(fCompile, "shader/FirstTriangle.frag.shader"); /*...*/ }