三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

VulkanSceneGraph学习教程(四)

VulkanSceneGraph学习教程(四)

第 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::ShaderModulevsg::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调整运行时编译行为:

字段含义
languageGLSL(默认)或HLSL
targetSPIR-V 目标版本(SPIRV_1_0SPIRV_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 &lt; 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; }

关键点说明:

  1. 宏定义控制:通过defines.insert("USE_NORMAL_MAP")添加宏,着色器源码中可使用#ifdef USE_NORMAL_MAP进行条件编译。
  2. 调试信息generateDebugInfo = true让 RenderDoc 等工具能显示原始变量名,而非优化后的临时变量。
  3. 版本控制target = SPIRV_1_5defaultVersion = 460确保使用较新的着色器特性。
  4. 统一设置:同一个ShaderCompileSettings对象可复用于多个着色器阶段,保证宏定义和编译选项的一致性。
  5. 条件编译:示例展示了如何根据宏定义在着色器中启用/禁用特定功能(如法线贴图、调试效果)。

这个示例可以直接集成到第 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读出来是乱码/链接失败用错编译阶段或没加-VglslangValidator -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 &lt;&lt; "警告:着色器编译竟然成功了,但代码中故意包含了错误!" &lt;&lt; std::endl; } } catch (const vsg::Exception&amp; e) { // 捕获 VSG 异常,输出详细错误信息 std::cerr &lt;&lt; "================================================" &lt;&lt; std::endl; std::cerr &lt;&lt; "VSG 着色器编译异常捕获:" &lt;&lt; std::endl; std::cerr &lt;&lt; "异常类型: " &lt;&lt; typeid(e).name() &lt;&lt; std::endl; std::cerr &lt;&lt; "异常消息: " &lt;&lt; e.message &lt;&lt; std::endl; std::cerr &lt;&lt; "================================================" &lt;&lt; std::endl; // 尝试从异常消息中提取 glslang 的错误信息 std::string errorMsg = e.message; if (errorMsg.find("ERROR:") != std::string::npos) { std::cerr &lt;&lt; "\n详细错误分析:" &lt;&lt; std::endl; // 查找错误行号信息 size_t linePos = errorMsg.find(":"); if (linePos != std::string::npos &amp;&amp; linePos + 1 &lt; 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 &lt;&lt; "错误发生在第 " &lt;&lt; lineNum &lt;&lt; " 行附近" &lt;&lt; std::endl; // 根据行号给出具体建议 if (lineNum == 7) // 对应 gl_Position 行 { std::cerr &lt;&lt; "建议:检查第 7 行是否缺少分号 (;)" &lt;&lt; std::endl; } else if (lineNum == 11) // 对应 undefinedVariable 行 { std::cerr &lt;&lt; "建议:检查第 11 行的函数或变量是否正确定义" &lt;&lt; std::endl; } } catch (...) { std::cerr &lt;&lt; "无法解析错误行号" &lt;&lt; std::endl; } } } // 输出完整的错误消息 std::cerr &lt;&lt; "\n完整错误消息:" &lt;&lt; std::endl; std::cerr &lt;&lt; errorMsg &lt;&lt; std::endl; } } catch (const std::exception&amp; e) { // 捕获其他标准异常 std::cerr &lt;&lt; "标准异常: " &lt;&lt; e.what() &lt;&lt; std::endl; } catch (...) { // 捕获所有其他异常 std::cerr &lt;&lt; "未知异常类型" &lt;&lt; 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-&gt;generateDebugInfo = true; std::cout &lt;&lt; "\n================================================" &lt;&lt; std::endl; std::cout &lt;&lt; "着色器编译信息日志示例:" &lt;&lt; std::endl; std::cout &lt;&lt; "================================================" &lt;&lt; std::endl; try { auto vertStage = vsg::ShaderStage::create( VK_SHADER_STAGE_VERTEX_BIT, "main", correctVertSource, compileSettings ); if (vertStage) { std::cout &lt;&lt; "✓ 着色器编译成功" &lt;&lt; std::endl; std::cout &lt;&lt; " 阶段: 顶点着色器" &lt;&lt; std::endl; std::cout &lt;&lt; " 入口点: main" &lt;&lt; std::endl; std::cout &lt;&lt; " SPIR-V 代码大小: " &lt;&lt; vertStage-&gt;module-&gt;code.size() &lt;&lt; " 字" &lt;&lt; std::endl; std::cout &lt;&lt; " 调试信息: " &lt;&lt; (compileSettings-&gt;generateDebugInfo ? "已生成" : "未生成") &lt;&lt; std::endl; } } catch (const vsg::Exception&amp; e) { std::cout &lt;&lt; "✗ 编译失败: " &lt;&lt; e.message &lt;&lt; std::endl; } } int main() { std::cout << "开始着色器错误排查演示..." << std::endl; // 演示错误捕获 testShaderCompileError(); // 演示成功编译的日志 logShaderCompilationInfo(); std::cout &lt;&lt; "\n错误排查建议:" &lt;&lt; std::endl; std::cout &lt;&lt; "1. 使用 try-catch 块包裹着色器创建代码" &lt;&lt; std::endl; std::cout &lt;&lt; "2. 捕获 vsg::Exception 类型异常" &lt;&lt; std::endl; std::cout &lt;&lt; "3. 检查异常消息中的 'ERROR:' 前缀和行号信息" &lt;&lt; std::endl; std::cout &lt;&lt; "4. 使用 glslangValidator 命令行工具预编译验证" &lt;&lt; std::endl; std::cout &lt;&lt; "5. 开启 generateDebugInfo 便于 RenderDoc 调试" &lt;&lt; std::endl; return 0; }

关键错误信息解析:

  1. glslang 错误格式:VSG 内部使用 glslang 编译 GLSL,错误消息通常包含ERROR: 0:行号: 错误描述格式。
  2. 常见错误类型
    • 语法错误:缺少分号、括号不匹配、关键字拼写错误
    • 语义错误:未定义的变量、类型不匹配、函数参数错误
    • 版本不兼容:使用了 Vulkan 不支持的 GLSL 特性
    • 资源限制:超出最大 uniform 数量、纹理单元限制等
  3. 处理建议
    • 将错误消息中的行号与原始 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的完整创建与配置。
← 返回列表