第 4 章 着色器准备说明
本章定位:Vulkan 只接受 SPIR-V,而 GLSL 是开发者最常写的语言。本章说清两者关系,以及 VSG 提供的两种着色器加载路径。理解它,第 5、9 章的管线代码才不突兀。
4.1 本章目标
- 理解为什么 Vulkan 需要 SPIR-V;
- 掌握 VSG 的两种着色器加载方式(运行时编译 / 离线预编译);
- 会用
glslangValidator离线把 GLSL 编译成.spv; - 了解
vsg::ShaderCompileSettings能调哪些编译选项。
4.2 前置准备
- 已按第 3 章装好 Vulkan SDK(含
glslangValidator); - 了解 GLSL 基础语法(顶点/片元着色器、
layout(location=)限定符)。
4.3 为什么是 SPIR-V?
Vulkan 的着色器阶段只接受 SPIR-V 中间码,不直接处理 GLSL/HLSL 源码。原因:
- SPIR-V 是二进制、无歧义、易校验,驱动无需再解析高级语言;
- 跨语言(GLSL/HLSL/MSL 都能编译成 SPIR-V),跨厂商一致。
因此「写 GLSL → 编译成 SPIR-V → 交给 Vulkan」是必不可少的一步。VSG 让这一步对你几乎无感知。
4.4 VSG 的两种加载方式
方式 A:运行时编译(推荐入门)
直接把 GLSL 字符串交给vsg::ShaderModule或vsg::ShaderStage,VSG 在compile()时用自带的glslang把 GLSL 编译成 SPIR-V:
// 直接传 GLSL 源码;VSG 内部用 glslang 编译为 SPIR-V auto vert = vsg::ShaderStage::create(VK_SHADER_STAGE_VERTEX_BIT, "main", R"( #version 450 layout(location = 0) in vec3 inPosition; void main() { gl_Position = vec4(inPosition, 1.0); } )");第 5 章的三角形就是这种方式——无需任何.spv文件。
对应的底层类型是
vsg::ShaderModule:构造ShaderModule(const std::string& source, ref_ptr<ShaderCompileSettings> hints = {})会把source暂存,编译时生成code(SPIR-Vstd::vector<uint32_t>)。
方式 B:离线预编译.spv
把 GLSL 先用工具编译成.spv,再用vsg::ShaderModule::read()从文件读取已编译的 SPIR-V:
auto vert = vsg::ShaderModule::read("shaders/triangle.vert.spv"); // 直接读 SPIR-V适合:着色器很大/很固定、或想绕开运行时编译开销、或需要在 CI 里静态校验着色器。
4.5 离线编译命令
用 Vulkan SDK 自带的glslangValidator:
# 顶点着色器 glslangValidator -V shaders/triangle.vert -o shaders/triangle.vert.spv # 片元着色器 glslangValidator -V shaders/triangle.frag -o shaders/triangle.frag.spv或用glslc(Google 的 Shaderc 前端,SDK 也常带):
glslc shaders/triangle.vert -o shaders/triangle.vert.spv⚠️ 记得 GLSL 顶部写
#version 450,且与着色器阶段匹配(-S vert/glslc会自动识别扩展名)。
4.6 编译选项:vsg::ShaderCompileSettings
若用方式 A,可通过ShaderCompileSettings调整运行时编译行为:
| 字段 | 含义 |
|---|---|
language | GLSL(默认)或HLSL |
target | SPIR-V 目标版本(SPIRV_1_0…SPIRV_1_5) |
defaultVersion | 默认 GLSL 版本(默认 450) |
defines | 预定义宏集合(std::set<std::string>) |
generateDebugInfo | 生成调试信息(便于 RenderDoc 查看变量名) |
optimize | 是否优化 SPIR-V |
示例:给所有着色器加一个USE_LIGHTING宏:
auto hints = vsg::ShaderCompileSettings::create(); hints->defines.insert("USE_LIGHTING"); auto frag = vsg::ShaderStage::create(VK_SHADER_STAGE_FRAGMENT_BIT, "main", fragSrc, hints);实战示例:自定义编译设置
下面是一个完整的实战代码示例,展示如何创建一个包含自定义宏定义、开启调试信息、并指定目标版本的vsg::ShaderCompileSettings对象,并将其应用于具体的顶点和片元着色器加载场景中:
#include <vsg/all.h> // 创建自定义编译设置 auto compileSettings = vsg::ShaderCompileSettings::create(); // 1. 设置目标 SPIR-V 版本为 1.5(支持 Vulkan 1.2+) compileSettings->target = vsg::ShaderCompileSettings::SPIRV_1_5; // 2. 设置默认 GLSL 版本为 460(支持更多现代特性) compileSettings->defaultVersion = 460; // 3. 添加自定义宏定义 compileSettings->defines.insert("USE_NORMAL_MAP"); // 法线贴图宏 compileSettings->defines.insert("MAX_LIGHTS=4"); // 最大灯光数 compileSettings->defines.insert("ENABLE_DEBUG"); // 调试宏 // 4. 开启调试信息(便于 RenderDoc 查看变量名) compileSettings->generateDebugInfo = true; // 5. 开启优化(生产环境推荐) compileSettings->optimize = true; // 顶点着色器源码(带条件编译) std::string vertSource = R"( #version 460 layout(location = 0) in vec3 inPosition; layout(location = 1) in vec3 inNormal; layout(location = 2) in vec2 inTexCoord; #ifdef USE_NORMAL_MAP layout(location = 3) in vec3 inTangent; #endif layout(location = 0) out vec3 fragNormal; layout(location = 1) out vec2 fragTexCoord; #ifdef USE_NORMAL_MAP layout(location = 2) out vec3 fragTangent; #endif void main() { gl_Position = vec4(inPosition, 1.0); fragNormal = inNormal; fragTexCoord = inTexCoord; #ifdef USE_NORMAL_MAP fragTangent = inTangent; #endif #ifdef ENABLE_DEBUG // 调试信息:输出顶点位置 gl_Position.y += 0.1 * sin(gl_Position.x * 10.0); #endif } )"; // 片元着色器源码(带条件编译) std::string fragSource = R"( #version 460 layout(location = 0) in vec3 fragNormal; layout(location = 1) in vec2 fragTexCoord; #ifdef USE_NORMAL_MAP layout(location = 2) in vec3 fragTangent; layout(binding = 1) uniform sampler2D normalMap; #endif layout(binding = 0) uniform sampler2D diffuseMap; layout(location = 0) out vec4 outColor; void main() { vec3 normal = normalize(fragNormal); #ifdef USE_NORMAL_MAP // 使用法线贴图 vec3 tangent = normalize(fragTangent); vec3 bitangent = cross(normal, tangent); mat3 TBN = mat3(tangent, bitangent, normal); vec3 normalMapValue = texture(normalMap, fragTexCoord).rgb * 2.0 - 1.0; normal = normalize(TBN * normalMapValue); #endif vec4 diffuse = texture(diffuseMap, fragTexCoord); // 简单光照计算 vec3 lightDir = normalize(vec3(0.5, 1.0, 0.5)); float diff = max(dot(normal, lightDir), 0.0); #ifdef MAX_LIGHTS // 多光源支持(示例) for (int i = 0; i < MAX_LIGHTS; ++i) { // 实际实现中这里会有真正的灯光计算 } #endif outColor = diffuse * (0.2 + 0.8 * diff); #ifdef ENABLE_DEBUG // 调试可视化:法线方向 outColor.rgb += normal * 0.1; #endif } )"; // 创建着色器阶段并应用编译设置 auto vertStage = vsg::ShaderStage::create( VK_SHADER_STAGE_VERTEX_BIT, // 着色器阶段 "main", // 入口函数名 vertSource, // GLSL 源码 compileSettings // 自定义编译设置 ); auto fragStage = vsg::ShaderStage::create( VK_SHADER_STAGE_FRAGMENT_BIT, "main", fragSource, compileSettings // 复用相同的编译设置 ); // 验证编译设置已生效 if (vertStage && fragStage) { std::cout << "顶点着色器编译设置:" << std::endl; std::cout << " - 目标版本: SPIR-V " << (compileSettings->target == vsg::ShaderCompileSettings::SPIRV_1_5 ? "1.5" : "其他") << std::endl; std::cout << " - GLSL 版本: " << compileSettings->defaultVersion << std::endl; std::cout << " - 调试信息: " << (compileSettings->generateDebugInfo ? "开启" : "关闭") << std::endl; std::cout << " - 优化: " << (compileSettings->optimize ? "开启" : "关闭") << std::endl; std::cout << " - 宏定义: "; for (const auto& define : compileSettings->defines) { std::cout << define << " "; } std::cout << std::endl; }关键点说明:
- 宏定义控制:通过
defines.insert("USE_NORMAL_MAP")添加宏,着色器源码中可使用#ifdef USE_NORMAL_MAP进行条件编译。 - 调试信息:
generateDebugInfo = true让 RenderDoc 等工具能显示原始变量名,而非优化后的临时变量。 - 版本控制:
target = SPIRV_1_5和defaultVersion = 460确保使用较新的着色器特性。 - 统一设置:同一个
ShaderCompileSettings对象可复用于多个着色器阶段,保证宏定义和编译选项的一致性。 - 条件编译:示例展示了如何根据宏定义在着色器中启用/禁用特定功能(如法线贴图、调试效果)。
这个示例可以直接集成到第 5 章的三角形程序或第 9 章的完整管线中,为实际项目中的着色器条件编译和调试提供参考模板。
4.7#include展开
GLSL 里若用了#include "common.glsl",VSG 提供vsg::insertIncludes(source, options)在编译前把被包含文件内联展开(按Options指定的搜索路径查找)。这对复用光照/材质代码片段很有用。
4.8 常见问题
| 现象 | 原因 | 解决 |
|---|---|---|
运行时报SPIRV-Tools/glslang 错误 | GLSL 语法不支持(如用了#version 300 es) | 改为#version 450;避免在 Vulkan 用桌面 GL 专用语法 |
.spv读出来是乱码/链接失败 | 用错编译阶段或没加-V | 用glslangValidator -V明确产出 SPIR-V |
| RenderDoc 里看不到变量名 | 没生成调试信息 | 设hints->generateDebugInfo = true |
| 多个着色器宏不一致 | defines没统一设 | 把共享ShaderCompileSettings复用到多个ShaderStage |
4.8.1 实战错误排查示例
当使用 VSG 的运行时编译方式时,如果 GLSL 代码有语法错误,VSG 会抛出异常。下面是一个完整的 C++ 示例,展示如何捕获并输出详细的着色器编译错误信息:
#include <vsg/all.h> #include <iostream> #include <stdexcept> // 尝试编译有错误的 GLSL 着色器,并捕获异常 void testShaderCompileError() { try { // 故意写一个有语法错误的 GLSL 顶点着色器 std::string errorVertSource = R"( #version 450 layout(location = 0) in vec3 inPosition; // 错误1:缺少分号 void main() { gl_Position = vec4(inPosition, 1.0) // 这里故意漏了分号 } // 错误2:使用了未定义的变量 vec3 undefinedVariable = someUndefinedFunction(); // someUndefinedFunction 不存在 )"; // 尝试创建着色器阶段(这会触发编译) auto vertStage = vsg::ShaderStage::create( VK_SHADER_STAGE_VERTEX_BIT, "main", errorVertSource ); // 如果编译成功(不应该发生),输出提示 if (vertStage) { std::cout << "警告:着色器编译竟然成功了,但代码中故意包含了错误!" << std::endl; } } catch (const vsg::Exception& e) { // 捕获 VSG 异常,输出详细错误信息 std::cerr << "================================================" << std::endl; std::cerr << "VSG 着色器编译异常捕获:" << std::endl; std::cerr << "异常类型: " << typeid(e).name() << std::endl; std::cerr << "异常消息: " << e.message << std::endl; std::cerr << "================================================" << std::endl; // 尝试从异常消息中提取 glslang 的错误信息 std::string errorMsg = e.message; if (errorMsg.find("ERROR:") != std::string::npos) { std::cerr << "\n详细错误分析:" << std::endl; // 查找错误行号信息 size_t linePos = errorMsg.find(":"); if (linePos != std::string::npos && linePos + 1 < errorMsg.length()) { // 尝试提取行号(glslang 错误格式通常是 "ERROR: 0:行号: 错误描述") std::string afterColon = errorMsg.substr(linePos + 1); size_t secondColon = afterColon.find(":"); if (secondColon != std::string::npos) { std::string lineNumStr = afterColon.substr(0, secondColon); try { int lineNum = std::stoi(lineNumStr); std::cerr << "错误发生在第 " << lineNum << " 行附近" << std::endl; // 根据行号给出具体建议 if (lineNum == 7) // 对应 gl_Position 行 { std::cerr << "建议:检查第 7 行是否缺少分号 (;)" << std::endl; } else if (lineNum == 11) // 对应 undefinedVariable 行 { std::cerr << "建议:检查第 11 行的函数或变量是否正确定义" << std::endl; } } catch (...) { std::cerr << "无法解析错误行号" << std::endl; } } } // 输出完整的错误消息 std::cerr << "\n完整错误消息:" << std::endl; std::cerr << errorMsg << std::endl; } } catch (const std::exception& e) { // 捕获其他标准异常 std::cerr << "标准异常: " << e.what() << std::endl; } catch (...) { // 捕获所有其他异常 std::cerr << "未知异常类型" << std::endl; } } // 使用 VSG 的日志系统记录着色器编译信息 void logShaderCompilationInfo() { // 创建一个正确的着色器来演示成功编译的日志 std::string correctVertSource = R"( #version 450 layout(location = 0) in vec3 inPosition; layout(location = 1) in vec3 inColor; layout(location = 0) out vec3 fragColor; void main() { gl_Position = vec4(inPosition, 1.0); fragColor = inColor; } )"; // 创建编译设置并开启调试信息 auto compileSettings = vsg::ShaderCompileSettings::create(); compileSettings->generateDebugInfo = true; std::cout << "\n================================================" << std::endl; std::cout << "着色器编译信息日志示例:" << std::endl; std::cout << "================================================" << std::endl; try { auto vertStage = vsg::ShaderStage::create( VK_SHADER_STAGE_VERTEX_BIT, "main", correctVertSource, compileSettings ); if (vertStage) { std::cout << "✓ 着色器编译成功" << std::endl; std::cout << " 阶段: 顶点着色器" << std::endl; std::cout << " 入口点: main" << std::endl; std::cout << " SPIR-V 代码大小: " << vertStage->module->code.size() << " 字" << std::endl; std::cout << " 调试信息: " << (compileSettings->generateDebugInfo ? "已生成" : "未生成") << std::endl; } } catch (const vsg::Exception& e) { std::cout << "✗ 编译失败: " << e.message << std::endl; } } int main() { std::cout << "开始着色器错误排查演示..." << std::endl; // 演示错误捕获 testShaderCompileError(); // 演示成功编译的日志 logShaderCompilationInfo(); std::cout << "\n错误排查建议:" << std::endl; std::cout << "1. 使用 try-catch 块包裹着色器创建代码" << std::endl; std::cout << "2. 捕获 vsg::Exception 类型异常" << std::endl; std::cout << "3. 检查异常消息中的 'ERROR:' 前缀和行号信息" << std::endl; std::cout << "4. 使用 glslangValidator 命令行工具预编译验证" << std::endl; std::cout << "5. 开启 generateDebugInfo 便于 RenderDoc 调试" << std::endl; return 0; }关键错误信息解析:
- glslang 错误格式:VSG 内部使用 glslang 编译 GLSL,错误消息通常包含
ERROR: 0:行号: 错误描述格式。 - 常见错误类型:
- 语法错误:缺少分号、括号不匹配、关键字拼写错误
- 语义错误:未定义的变量、类型不匹配、函数参数错误
- 版本不兼容:使用了 Vulkan 不支持的 GLSL 特性
- 资源限制:超出最大 uniform 数量、纹理单元限制等
- 处理建议:
- 将错误消息中的行号与原始 GLSL 代码行号对应(注意 #version 和空行也算行)
- 使用
glslangValidator -V yourShader.vert在命令行预编译验证 - 简化着色器代码,逐步添加功能定位问题
- 检查 GLSL 版本(Vulkan 需要 #version 450 或更高)
- 确保所有函数和变量都有正确定义
实际应用:在开发过程中,可以将上述错误捕获逻辑封装到工具函数中,在着色器加载失败时自动输出详细诊断信息,并尝试提供修复建议。
4.9 小结
- Vulkan 只吃 SPIR-V;VSG 让 GLSL→SPIR-V 对你几乎透明(方式 A 运行时编译);
- 想要离线/可控,就用
glslangValidator -V预编译成.spv,再用ShaderModule::read()读取(方式 B); ShaderCompileSettings控制版本、宏、调试信息;insertIncludes支持 GLSL#include。
4.10 延伸阅读与下一章预告
- 第 5 章《第一个 VSG 程序》:方式 A 落地,三角形 + 运行时编译管线;
- 第 9 章《着色器与管线》:
ShaderModule/PipelineLayout/GraphicsPipeline的完整创建与配置。