公司动态
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-VVulkan 的着色器阶段只接受 SPIR-V 中间码不直接处理 GLSL/HLSL 源码。原因SPIR-V 是二进制、无歧义、易校验驱动无需再解析高级语言跨语言GLSL/HLSL/MSL 都能编译成 SPIR-V跨厂商一致。因此「写 GLSL → 编译成 SPIR-V → 交给 Vulkan」是必不可少的一步。VSG 让这一步对你几乎无感知。4.4 VSG 的两种加载方式方式 A运行时编译推荐入门直接把 GLSL 字符串交给vsg::ShaderModule或vsg::ShaderStageVSG 在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_ptrShaderCompileSettings hints {})会把source暂存编译时生成codeSPIR-Vstd::vectoruint32_t。方式 B离线预编译.spv把 GLSL 先用工具编译成.spv再用vsg::ShaderModule::read()从文件读取已编译的 SPIR-Vauto 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或用glslcGoogle 的 Shaderc 前端SDK 也常带glslc shaders/triangle.vert -o shaders/triangle.vert.spv⚠️ 记得 GLSL 顶部写#version 450且与着色器阶段匹配-S vert/glslc会自动识别扩展名。4.6 编译选项vsg::ShaderCompileSettings若用方式 A可通过ShaderCompileSettings调整运行时编译行为字段含义languageGLSL默认或HLSLtargetSPIR-V 目标版本SPIRV_1_0…SPIRV_1_5defaultVersion默认 GLSL 版本默认 450defines预定义宏集合std::setstd::stringgenerateDebugInfo生成调试信息便于 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_LIGHTS4); // 最大灯光数 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; }关键点说明宏定义控制通过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.glslVSG 提供vsg::insertIncludes(source, options)在编译前把被包含文件内联展开按Options指定的搜索路径查找。这对复用光照/材质代码片段很有用。4.8 常见问题现象原因解决运行时报SPIRV-Tools/glslang 错误GLSL 语法不支持如用了#version 300 es改为#version 450避免在 Vulkan 用桌面 GL 专用语法.spv读出来是乱码/链接失败用错编译阶段或没加-V用glslangValidator -V明确产出 SPIR-VRenderDoc 里看不到变量名没生成调试信息设hints-generateDebugInfo true多个着色器宏不一致defines没统一设把共享ShaderCompileSettings复用到多个ShaderStage4.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::Exceptionamp; 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::exceptionamp; 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::Exceptionamp; 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; }关键错误信息解析glslang 错误格式VSG 内部使用 glslang 编译 GLSL错误消息通常包含ERROR: 0:行号: 错误描述格式。常见错误类型语法错误缺少分号、括号不匹配、关键字拼写错误语义错误未定义的变量、类型不匹配、函数参数错误版本不兼容使用了 Vulkan 不支持的 GLSL 特性资源限制超出最大 uniform 数量、纹理单元限制等处理建议将错误消息中的行号与原始 GLSL 代码行号对应注意 #version 和空行也算行使用glslangValidator -V yourShader.vert在命令行预编译验证简化着色器代码逐步添加功能定位问题检查 GLSL 版本Vulkan 需要 #version 450 或更高确保所有函数和变量都有正确定义实际应用在开发过程中可以将上述错误捕获逻辑封装到工具函数中在着色器加载失败时自动输出详细诊断信息并尝试提供修复建议。4.9 小结Vulkan 只吃 SPIR-VVSG 让 GLSL→SPIR-V 对你几乎透明方式 A 运行时编译想要离线/可控就用glslangValidator -V预编译成.spv再用ShaderModule::read()读取方式 BShaderCompileSettings控制版本、宏、调试信息insertIncludes支持 GLSL#include。4.10 延伸阅读与下一章预告第 5 章《第一个 VSG 程序》方式 A 落地三角形 运行时编译管线第 9 章《着色器与管线》ShaderModule/PipelineLayout/GraphicsPipeline的完整创建与配置。