本文主要是介绍Hazel游戏引擎(100)Vulkan、SPIR-V和新Shader系统,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文中若有代码、术语等错误,欢迎指正
文章目录
- 前言
- 介绍SPIR-V
- 项目改变
- 代码流程
- 给GPU的Uniform缓冲区上传数据
- SPIR-V编译
- 项目遇到耗费我一天半的BUG
- BUG信息以及解决方法
- 我解决此BUG的路线
- 运行脚本安装Vulkan遇到的问题
前言
-
前前言
-
Cherno未来想做的
当前项目以后Cherno打算支持vulkan,由于vulkan着色器代码也支持glsl语言,但是和Opengl的glsl标准不一。
因为Vulkan中存在OpenGL的不存在的东西(反之亦然),所以着色器代码肯定不同。
-
vulkan和opengl的glsl对比-以Uniform为例
-
主要不同
在于,opengl支持uniform,vulkan不支持uniform,而是支持uniform缓冲区(push_constant、存储缓冲区)这比OpenGL的glsl更好。
opengl的uniform
uniform mat4 m_transform;
vulkan的uniform缓冲区
layout(std140, binding=2) uniform Transform{mat4 Transform; }
-
Opengl支持的uniform哪里不好
场景有很多个物体,这样一个物体调用一次drawcall(不是批处理模式),每个物体需要显示都需要上传摄像机的投影视图矩阵,那么10000个物体就有10000个uniform更新,但其实摄像机的投影视图矩阵在当前帧不变的,这样就会造成性能的下降。
-
vulkan哪里好
vulkan的uniform是在GPU开辟的一块缓冲区,每个物体需要摄像机的投影视图矩阵,只需将这块缓冲区放入投影矩阵的值,然后每个drawcall都去访问这块缓冲区,得到投影视图矩阵就行,性能更好。
-
但后面我发现,OpenGL也有uniform缓冲区,不知是否我理解错了。
-
-
-
此节目的
重新写shader系统,使其能支持vulkan和opengl两种glsl
-
如何实现
使用SPIR-V,作为中间表示语言,即可支持vulkan也支持opengl的glsl
介绍SPIR-V
-
介绍SPIR-V
vulkanApi要求以SPIR-V组件的形式提供着色器,而这个SPIR-V相当于“中间表示语言”
- 可以将写好的glsl、hlsl转换为SPIR-V
- 也可以将SPIR-V转换为glsl、hlsl。
使得可以完成只写一次shader,自动生成各种不同版本的shader语言。
-
SPIR-V什么工作方式:(可能我理解错了了)
Cherno说,将vulkan的glsl用SPIR-V编译成SPIR-V二进制文件,然后可以用SPIR-V的交叉编译这个二进制文件可以成hlsl、metal,以及兼容OpenGL的glsl。
-
实现过程中的重要功能
使用SPIR-V加入着色器缓存功能,不用每次都编译着色器,节省时间。
以上很有可能说的不正确,我有点迷糊,东拼西凑的写完以上内容,但大概意思是这样。
项目改变
-
下载vulkan
地址
需要下载最新,安装的时候每个组件最好都点上,需要有这几个文件
-
环境变量
特别是VULKAN_SDK,接下来的premake会获取这个环境变量
-
修改premake
...... outputdir = "%{cfg.buildcfg}-%{cfg.system}-%{cfg.architecture}"VULKAN_SDK = os.getenv("VULKAN_SDK")-- Include directories relative to root folder (solution directory) IncludeDir = {} IncludeDir["GLFW"] = "GameEngineLightWeight/vendor/GLFW/include" ...... IncludeDir["ImGuizmo"] = "GameEngineLightWeight/vendor/ImGuizmo" IncludeDir["VulkanSDK"] = "%{VULKAN_SDK}/Include"LibraryDir = {}LibraryDir["VulkanSDK"] = "%{VULKAN_SDK}/Lib" LibraryDir["VulkanSDK_Debug"] = "%{wks.location}/GameEngineLightWeight/vendor/VulkanSDK/Lib"Library = {} Library["Vulkan"] = "%{LibraryDir.VulkanSDK}/vulkan-1.lib" Library["VulkanUtils"] = "%{LibraryDir.VulkanSDK}/VkLayer_utils.lib"Library["ShaderC_Debug"] = "%{LibraryDir.VulkanSDK_Debug}/shaderc_sharedd.lib" Library["SPIRV_Cross_Debug"] = "%{LibraryDir.VulkanSDK_Debug}/spirv-cross-cored.lib" Library["SPIRV_Cross_GLSL_Debug"] = "%{LibraryDir.VulkanSDK_Debug}/spirv-cross-glsld.lib" Library["SPIRV_Tools_Debug"] = "%{LibraryDir.VulkanSDK_Debug}/SPIRV-Toolsd.lib"Library["ShaderC_Release"] = "%{LibraryDir.VulkanSDK}/shaderc_shared.lib" Library["SPIRV_Cross_Release"] = "%{LibraryDir.VulkanSDK}/spirv-cross-core.lib" Library["SPIRV_Cross_GLSL_Release"] = "%{LibraryDir.VulkanSDK}/spirv-cross-glsl.lib"group "Dependencies"include "GameEngineLightWeight/vendor/GLFW" ...... project "GameEngineLightWeight"location "GameEngineLightWeight"includedirs{......"%{IncludeDir.ImGuizmo}","%{IncludeDir.VulkanSDK}"}......filter "configurations:Debug"......links{"%{Library.ShaderC_Debug}","%{Library.SPIRV_Cross_Debug}","%{Library.SPIRV_Cross_GLSL_Debug}"}filter "configurations:Release"......links{"%{Library.ShaderC_Release}","%{Library.SPIRV_Cross_Release}","%{Library.SPIRV_Cross_GLSL_Release}"}
注意,项目要取消动态链接改为静态链接了,可能是vulkan的那几个lib文件是静态的
kind "StaticLib" -- staticruntime "on" staticruntime "off"
代码流程
给GPU的Uniform缓冲区上传数据
-
写vulkan的glsl
// 纹理的glsl #type vertex #version 450 corelayout(location = 0) in vec3 a_Position; layout(location = 1) in vec4 a_Color; layout(location = 2) in vec2 a_TexCoord; layout(location = 3) in float a_TexIndex; layout(location = 4) in float a_TilingFactor; layout(location = 5) in int a_EntityID;// // uniform缓冲区/// // 使用0号缓冲区/// layout(std140, binding = 0) uniform Camera{ // std140是布局mat4 u_ViewProjection; }; // uniform mat4 u_ViewProjection; struct VertexOutput {vec4 Color;vec2 TexCoord;float TexIndex;float TilingFactor; }; layout(location = 0) out VertexOutput Output; layout(location = 4) out flat int v_EntityID; // 4 是因为TilingFactor 是3 //out vec4 v_Color; //out vec2 v_TexCoord; //out float v_TexIndex; //out float v_TilingFactor; //out flat int v_EntityId;void main() {/*v_Color = a_Color;v_TexCoord = a_TexCoord;v_TexIndex = a_TexIndex;v_TilingFactor = a_TilingFactor;v_EntityId = a_EntityId;*/Output.Color = a_Color;Output.TexCoord = a_TexCoord;Output.TexIndex = a_TexIndex;Output.TilingFactor = a_TilingFactor;v_EntityID = a_EntityID;gl_Position = u_ViewProjection * vec4(a_Position, 1.0); }#type fragment #version 450 corelayout(location = 0) out vec4 color; layout(location = 1) out int color2;struct VertexOutput {vec4 Color;vec2 TexCoord;float TexIndex;float TilingFactor; }; layout(location = 0) in VertexOutput Input; layout(location = 4) in flat int v_EntityID; // 4 是因为TilingFactor 是3//in vec4 v_Color; //in vec2 v_TexCoord; //in float v_TexIndex; //in float v_TilingFactor; //in flat int v_EntityId;layout(binding = 0) uniform sampler2D u_Textures[32]; //uniform sampler2D u_Textures[32];void main() {color = texture(u_Textures[int(Input.TexIndex)], Input.TexCoord * Input.TilingFactor) * Input.Color;color2 = v_EntityID;// 给顶点包围的区域每个像素都设置为实体ID }
-
std140是什么
是一种布局,点这了解
-
注意被注释的是OpenGL的Glsl写法
-
-
新建UniformBuffer类
#pragma once #include "Hazel/Core/Core.h"namespace Hazel {class UniformBuffer{public:virtual ~UniformBuffer(){}virtual void SetData(const void* data, uint32_t size, uint32_t offset = 0) = 0;static Ref<UniformBuffer> Create(uint32_t size, uint32_t binding);}; }
#include "hzpch.h" #include "OpenGLUniformBuffer.h" #include <glad/glad.h>namespace Hazel {//// Uniform缓冲区/OpenGLUniformBuffer::OpenGLUniformBuffer(uint32_t size, uint32_t binding){// GPU创建缓冲区,并且返回缓冲区号m_RendererIDglCreateBuffers(1, &m_RendererID);// 声明缓冲区的数据glNamedBufferData(m_RendererID, size, nullptr, GL_DYNAMIC_DRAW); // TODO:investigate usage hint// 将在glsl上设置的bingding 0号缓冲区与真正的缓冲区m_RendererID联系起来glBindBufferBase(GL_UNIFORM_BUFFER, binding, m_RendererID);/* glsl代码中声明使用0号的uniform缓冲区layout(std140, binding = 0) uniform Camera*/}OpenGLUniformBuffer::~OpenGLUniformBuffer(){glDeleteBuffers(1, &m_RendererID);}// 上传数据给缓冲区///void OpenGLUniformBuffer::SetData(const void* data, uint32_t size, uint32_t offset){// 上传数据给m_RendererID号缓冲区吧,实则给GPU的bingding号缓冲区glNamedBufferSubData(m_RendererID, offset, size, data);} }
由上,glBindBufferBase,将使glsl上设置的bingding 0号缓冲区与真正的缓冲区m_RendererID联系起来
-
Renderer2D.cpp上传摄像机的投影视图矩阵给Uniform缓冲区
struct Renderer2DData {.....struct CameraData{glm::mat4 ViewProjection;};CameraData CameraBuffer;Ref<UniformBuffer> CameraUniformBuffer; };void Renderer2D::Init() {.....// 初始化CameraUniformBuffer实例,在构造函数中就将调用上面的glBindBufferBase函数s_Data.CameraUniformBuffer = UniformBuffer::Create(sizeof(Renderer2DData::CameraData), 0); } void Renderer2D::BeginScene(const EditorCamera& camera) {HZ_PROFILE_FUNCTION();s_Data.CameraBuffer.ViewProjection = camera.GetViewProjection();// 上传数据给缓冲区///// 使用CameraUniformBuffer的SetData才真正的上传投影视图矩阵给GPU的Uniform缓冲区s_Data.CameraUniformBuffer->SetData(&s_Data.CameraBuffer, sizeof(Renderer2DData::CameraData));StartBatch(); }
给CameraUniformBuffer的m_RendererID缓冲区上传数据,也是上传到glsl上声明使用0号uniform缓冲区上
layout(std140, binding = 0) uniform Camera
SPIR-V编译
-
将vulkan的glsl编译成SPIR-V二进制文件,并且保存在本地作为缓存
void OpenGLShader::CompileOrGetVulkanBinaries(const std::unordered_map<GLenum, std::string>& shaderSources) {GLuint program = glCreateProgram();shaderc::Compiler compiler;shaderc::CompileOptions options;options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_2);const bool optimize = true;if (optimize) {// 优化:性能优先options.SetOptimizationLevel(shaderc_optimization_level_performance);}// 生成二进制的缓存目录std::filesystem::path cacheDirectory = Utils::GetCacheDirectory();auto& shaderData = m_VulkanSPIRV;shaderData.clear();for (auto&& [stage, source] : shaderSources){std::filesystem::path shaderFilePath = m_FilePath;std::filesystem::path cachedPath = cacheDirectory / (shaderFilePath.filename().string() + Utils::GLShaderStageCachedOpenGLFileExtension(stage));std::ifstream in(cachedPath, std::ios::in | std::ios::binary);// 缓存是否存在if (in.is_open()) {// 存在打开加载in.seekg(0, std::ios::end);auto size = in.tellg();in.seekg(0, std::ios::beg);auto& data = shaderData[stage];// ?data.resize(size / sizeof(uint32_t));in.read((char*)data.data(), size);}else {/// 将Vulkan的glsl编译成SPIR-V二进制文件shaderc::SpvCompilationResult module = compiler.CompileGlslToSpv(source, Utils::GLShaderStageToShaderC(stage), m_FilePath.c_str(), options);if (module.GetCompilationStatus() != shaderc_compilation_status_success) {HZ_CORE_ERROR(module.GetErrorMessage());HZ_CORE_ASSERT(false);}shaderData[stage] = std::vector<uint32_t>(module.cbegin(), module.cend());std::ofstream out(cachedPath, std::ios::out | std::ios::binary);if (out.is_open()) {auto& data = shaderData[stage];out.write((char*)data.data(), data.size() * sizeof(uint32_t));out.flush();out.close();}}}for (auto&& [stage, data] : shaderData)Reflect(stage, data); }
-
将Vulkan的glsl的SPIR-V二进制文件转换为OpenGL的glsl源文件字符串
再将OpenGL的glsl源文件字符串转换为SPIR-V二进制文件,并且保存在本地作为缓存
void OpenGLShader::CompileOrGetOpenGLBinaries() {auto& shaderData = m_OpenGLSPIRV;shaderc::Compiler compiler;shaderc::CompileOptions options;options.SetTargetEnvironment(shaderc_target_env_vulkan, shaderc_env_version_vulkan_1_3);const bool optimize = true;if (optimize) {// 优化:性能优先options.SetOptimizationLevel(shaderc_optimization_level_performance);}// 生成二进制的缓存目录std::filesystem::path cacheDirectory = Utils::GetCacheDirectory();shaderData.clear();m_OpenGLSourceCode.clear();for (auto&& [stage, spirv] : m_VulkanSPIRV){std::filesystem::path shaderFilePath = m_FilePath;std::filesystem::path cachedPath = cacheDirectory / (shaderFilePath.filename().string() + Utils::GLShaderStageCachedOpenGLFileExtension(stage));std::ifstream in(cachedPath, std::ios::in | std::ios::binary);// 缓存是否存在if (in.is_open()) {// 存在打开加载in.seekg(0, std::ios::end);auto size = in.tellg();in.seekg(0, std::ios::beg);auto& data = shaderData[stage];// ?data.resize(size / sizeof(uint32_t));in.read((char*)data.data(), size);}else {/// 将Vulkan的glsl的SPIR-V二进制文件转换为OpenGL的glsl源文件字符串spirv_cross::CompilerGLSL glslCompiler(spirv);m_OpenGLSourceCode[stage] = glslCompiler.compile();auto& source = m_OpenGLSourceCode[stage];/// 再将OpenGL的glsl源文件字符串转换为SPIR-V二进制文件,并且保存在本地作为缓存shaderc::SpvCompilationResult module = compiler.CompileGlslToSpv(source, Utils::GLShaderStageToShaderC(stage), m_FilePath.c_str(), options);if (module.GetCompilationStatus() != shaderc_compilation_status_success) {HZ_CORE_ERROR(module.GetErrorMessage());HZ_CORE_ASSERT(false);}shaderData[stage] = std::vector<uint32_t>(module.cbegin(), module.cend());std::ofstream out(cachedPath, std::ios::out | std::ios::binary);if (out.is_open()) {auto& data = shaderData[stage];out.write((char*)data.data(), data.size() * sizeof(uint32_t));out.flush();out.close();}}} }
-
用OpenGL的API编译链接OpenGL版本的glsl的SPIR-V二进制文件
与原始编译链接OpenGL的GLSL着色器代码不太一样
void OpenGLShader::CreateProgram() {GLuint program = glCreateProgram();std::vector<GLuint> shaderIDs;for (auto&& [stage, spirv] : m_OpenGLSPIRV){GLuint shaderID = shaderIDs.emplace_back(glCreateShader(stage));glShaderBinary(1, &shaderID, GL_SHADER_BINARY_FORMAT_SPIR_V, spirv.data(), spirv.size() * sizeof(uint32_t));glSpecializeShader(shaderID, "main", 0, nullptr, nullptr);glAttachShader(program, shaderID);}glLinkProgram(program);GLint isLinked;glGetProgramiv(program, GL_LINK_STATUS, &isLinked);if (isLinked == GL_FALSE){GLint maxLength;glGetProgramiv(program, GL_INFO_LOG_LENGTH, &maxLength);std::vector<GLchar> infoLog(maxLength);glGetProgramInfoLog(program, maxLength, &maxLength, infoLog.data());HZ_CORE_ERROR("Shader linking failed ({0}):\n{1}", m_FilePath, infoLog.data());glDeleteProgram(program);for (auto id : shaderIDs)glDeleteShader(id);}for (auto id : shaderIDs){glDetachShader(program, id);glDeleteShader(id);}m_RendererID = program; }
项目遇到耗费我一天半的BUG
BUG信息以及解决方法
-
错误如下
0x00007FFE06F0FDB6 (atio6axx.dll)处(位于 GameEngine-Editor.exe 中)引发的异常: 0xC0000005: 写入位置 0x0000000000000008 时发生访问冲突。
-
错误原因
AMD显卡运行由OpenGL的SPIR-V二进制编译的shader会报这个错,是AMD显卡的问题!
-
解决方法一
显卡设置用英伟达显卡运行此程序解决
-
解决方法二:适合只有AMD显卡的电脑
来自Github上Hazel的ISSUE:点这
解决方法来自ISSUE fixed:点这
写openglshader代码时,用Vulkan的SPIR-V二进制编译shader。
大意是:使用 SpirV 和反射的唯一解决方案是使用 SpirV-Cross 为 OpenGL 生成的 sharder 源,而不是 spirV 二进制文件。
For now the only solution i found to keep using SpirV and the reflection is by using sharder sources generated by SpirV-Cross for OpenGL instead fof the spirV binary.
GLuint program = glCreateProgram();std::vector<GLuint> glShadersIDs;int glShaderIDIndex = 0; for (auto&& [stage, spirv] : m_VulkanSPIRV) {spirv_cross::CompilerGLSL glslCompiler(spirv);auto& source = glslCompiler.compile();GLuint shader;shader = glCreateShader(stage);const GLchar* sourceCStr = source.c_str();glShaderSource(shader, 1, &sourceCStr, 0);glCompileShader(shader);int isCompiled = 0;glGetShaderiv(shader, GL_COMPILE_STATUS, &isCompiled);if (isCompiled == GL_FALSE) {int maxLength = 0;glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &maxLength);std::vector<char> infoLog(maxLength);glGetShaderInfoLog(shader, maxLength, &maxLength, &infoLog[0]);glDeleteShader(shader);HZ_CORE_ERROR("{0}", infoLog.data());HZ_CORE_ASSERT(false, "[OpenGL] Shader compilation failure!");break;}glAttachShader(program, shader);glShadersIDs[glShaderIDIndex++] = shader; }
我还没完整运行起来。
第二种方法比较靠谱:检测是amd显卡就换
我解决此BUG的路线
-
以为是代码写错
下载了TheCherno的项目并且修复好运行起来
-
克隆Hazel项目到本地
git clone https://github.com/TheCherno/Hazel
2023年2月8日补:
上面的git命令因为缺少–recursive所以没成功拷贝子模块,用下面的命令,可以省去下面修 复子模块 的步骤
git clone --recursive https://github.com/TheCherno/Hazel
-
回退到第100集版本
到Hazel项目的提交历史复制SHA
运行以下命令,即可回退
git checkout sha
-
修复子模块
将.gitmodules文件替换为正确的url地址
[submodule "Hazel/vendor/spdlog"]path = Hazel/vendor/spdlogurl = https://github.com/gabime/spdlog [submodule "Hazel/vendor/GLFW"]path = Hazel/vendor/GLFWurl = https://github.com/TheCherno/glfw [submodule "Hazel/vendor/imgui"]path = Hazel/vendor/imguiurl = https://github.com/TheCherno/imgui [submodule "Hazel/vendor/glm"]path = Hazel/vendor/glmurl = https://github.com/g-truc/glm [submodule "Hazel/vendor/yaml-cpp"]path = Hazel/vendor/yaml-cppurl = https://github.com/TheCherno/yaml-cpp [submodule "Hazel/vendor/ImGuizmo"]path = Hazel/vendor/ImGuizmourl = https://github.com/TheCherno/ImGuizmo
再运行下载子模块git命令
git submodule init git submodule update
-
然后拷贝vulkan的lib,重新指向premake脚本,即可
运行发现还是报同样的错,遂放弃。
-
-
以为是vulkan版本错误
卸载最新的1.3.236.0版本
-
安装1.2.170.0版本
但是发现这个版本竟没有Cherno要求的shaderc_sharedd.lib,即后带d的lib文件,于是混用1.3.236.0的shaderc_sharedd.lib文件,运行不出意外还是报错,直接运行不起来,即卸载
难怪Cherno的python脚本写了验证是否有shaderc_sharedd.lib文件
@classmethod def CheckVulkanSDKDebugLibs(cls):vulkanSDK = os.environ.get("VULKAN_SDK")shadercdLib = Path(f"{vulkanSDK}/Lib/shaderc_sharedd.lib")return shadercdLib.exists()
-
安装1.3.216.0版本
问题和上一样没有shaderc_sharedd.lib,混用1.3.236.0的shaderc_sharedd.lib文件,报错。即卸载
error LNK2001: 无法解析的外部符号 "protected: virtual void __cdecl spirv_cross::CompilerGLSL::declare_undefined_values(void)" (?declare_undefined_values@CompilerGLSL@spirv_cross@@MEAAXXZ)
-
-
收获
- 解决方法来自Hazel的GitHub的ISSUE,而我却忘记遇到bug应该在这里找类似的问题。
- 搜索谷歌问题有技巧,应该用atio6axx.dll关键字搜索,我用一整段错误信息搜索找不到合适解决方法。
即:最好先好好查谷歌、GitHub的issue再折腾其它方法。
运行脚本安装Vulkan遇到的问题
-
SyntaxError: (unicode error) ‘utf-8’ codec can’t decode byte 0xa3
python程序中使用了UTF-8编码,而没有在脚本上申明要使用UTF-8编码。说的更加直接一点,就是用了中文字符,一般我们习惯使用中文作为注释。
在python脚本程序中,主动申明我们使用UTF-8编码方式。
申明的方法如下,在程序最上面增加以下语句,尤其是第二句。
#!/usr/bin/python # -*- coding: UTF-8 -*-
-
ssl.SSLEOFError: EOF occurred in violation of protocol (_ssl.c:1129)
搜索一番发现是因为电脑开了代理(科学上网工具)的原因,但是实际上代理是可以正常使用的。
方法:关闭科学上网
-
安装了Vulkan但是python脚本却检测不到
确定在环境变量设置了VULKAN_SDK名称的环境变量
VULKAN_SDK D:\3DGameSDK\Vulkan\1.3.236.0
如果设置了,还是检测不到,就重启IDE
这篇关于Hazel游戏引擎(100)Vulkan、SPIR-V和新Shader系统的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!