Shader modules
与早期的API不同,Vulkan中的着色器代码必须以字节码格式指定,而不是人类可读的语法,如GLSL和HLSL。这种字节码格式称为SPIR-V它是一种可用于编写图形和计算着色器的格式
使用像SPIR-V这样简单的字节码格式,不会面临其他供应商的驱动程序因语法错误而拒绝您的代码的风险,
SPIR-V二进制文件,并不意味着我们需要手工编写,Khronos已经发布了他们自己的独立于供应商的glslangValidator.exe编译器,该编译器将GLSL编译为SPIR-V
但我们将使用Google的glslc.exe,glslc的优点是它使用与GCC和Clang等知名编译器相同的参数格式,并且这两个工具都已包含在Vulkan SDK中,因此您无需下载任何额外的内容。
GLSL是一种具有C风格语法的着色语言。用它编写的程序有一个main函数,GLSL使用全局变量来处理输入和输出,而不是使用参数作为输入和返回值作为输出,该语言包括内置的矢量和矩阵处理功能
Vertex shader
顶点着色器处理每个传入的顶点。它将其属性,如position, color, normal and texture coordinates作为输入。输出是clip coordinate裁剪坐标中的最终位置以及需要传递给片段着色器的属性,这些值然后将被光栅化器在片段上插值以产生 smooth平滑的梯度
clip coordinate裁剪坐标(单位立方体)是来自顶点着色器的四维向量,随后通过将整个向量除以其最后一个分量w来将其转换为规范化的设备坐标(单位正方形)->屏幕坐标(长方形)
左边时帧缓冲坐标(像素坐标),右边时标准化设备坐标,它和opengl 不同,y坐标是相反的,z坐标的范围仅是0---1
对于我们的第一个三角形,我们将直接指定三个顶点的位置作为归一化的设备坐标
通常情况下,这些坐标或者其他顶点属性将存储在顶点缓冲区中,但在Vulkan中创建顶点缓冲区并填充数据并非易事
文件
在vs中新建.vert和.frag的后缀名文件(可以任意取)
首先指明#version的glsl版本
创建元素数量为3的vec2二维向量类型的位置数组,和颜色数组
每个顶点都会调用main()函数,内置的gl_VertexIndex变量包含当前顶点的索引,我们通过positions[gl_VertexIndex]去从数组访问顶点的位置,
并传入w分量,通过内置变量gl_Position用作vertex shader的输出
需要通过out将顶点颜色属性传给fragment shader,并在fragment shader中in匹配输入(不必同名只要location索引一致就行)
cpp
#version 450
layout(location = 0) out vec3 fragColor;
vec2 positions[3] = vec2[](
vec2(0.0, -0.5),
vec2(0.5, 0.5),
vec2(-0.5, 0.5)
);
vec3 colors[3] = vec3[](
vec3(1.0, 0.0, 0.0),
vec3(0.0, 1.0, 0.0),
vec3(0.0, 0.0, 1.0)
);
void main() {
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
fragColor = colors[gl_VertexIndex];
}
fragment shader
用fragment 填充屏幕上的区域,在这些fragment上调用fragment shader以产生frambuffer的color and depth
main函数会为每个片段调用,就像顶点着色器main函数会为每个顶点调用一样
没有内置变量来输出当前计算的颜色(GLSL中的颜色是4分量向量),必须为每个frambuffer指定自己的输出变量,比如这里的outColor
其中layout(location = 0)布局修饰符 指定frambuffer的索引
fragColor的值将自动 为三个顶点之间的片段插值,从而产生平滑的颜色渐变
cpp
#version 450
layout(location = 0) in vec3 fragColor;
layout(location = 0) out vec4 outColor;
void main() {
outColor = vec4(fragColor, 1.0);
}
编译为SPIR_V字节码
我们现在将使用glslc程序将这些代码编译成SPIR-V字节码
首先创建一个bat文件(批处理文件),并将它放在你的shader文件夹下,
在其中写入,保存后,双击运行,至此我们的目录下就编译完成了spv文件
这两个命令告诉glslc编译器读取GLSL源文件,并使用-o(输出)标志输出SPIR-V字节码文件
如果着色器包含语法错误,那么编译器将告诉您行号和问题
在命令行上编译着色器是最简单的选项之一,但是也可以用Vulkan SDK的libshaderc,这是一个用于从程序中将GLSL代码编译为SPIR-V的库
cpp
....../glslc.exe .......vert -o vert.spv
....../glslc.exe .......frag -o frag.spv
pause
加载SPIR-V到程序中
在是时候将它们加载到我们的程序中,以便在某个时候将它们插入graphics pipeline
编写一个简单的readFile函数来从文件中加载二进制数据
对于输入流对象,我们指定两个标志
- ate:从文件末尾开始阅读(我们可以使用读取位置来确定文件的tellg()大小并分配缓冲区)
- binary:将文件读取为二进制文件(避免文本转换)
将读取的内容返回到由std::vector管理的字节数组中
cpp
static std::vector<char> readFile(const std::string& filename) {
std::ifstream file(filename, std::ios::ate | std::ios::binary);//文件输入流
if (!file.is_open()) {
throw std::runtime_error("failed to open file!");
}
size_t fileSize = (size_t)file.tellg();
std::vector<char> buffer(fileSize);//读取到的字节数组缓冲
file.seekg(0);
file.read(buffer.data(), fileSize);
file.close();
return buffer;
}
在createGraphicsPipeline调用函数来加载两个着色器的字节码、
创建Shader modules
在将SPIR-V的字节码传递到pipeline之前,我们必须将其包装在VkShaderModule对象中
同样遵守info和create,需要指定字节大小,和指向缓冲区的指针code.data(),
但字节码指针是uint32_t指针而不是char指针。因此,我们需要使用reinterpret_cast来强制转换指针
在createGraphicsPipeline调用,
Shader modules就像一个包装器,包装了char code,将SPIR-V字节码编译并链接到由GPU执行的机器码在图形管道创建之前不会发生,
一旦pipeline创建完成,我们就可以vkDestroyShaderModule Shader modules,在createGraphicsPipeline所有指令后写
cpp
VkShaderModule createShaderModule(const std::vector<char>& code) {
VkShaderModuleCreateInfo createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();
createInfo.pCode = reinterpret_cast<const uint32_t*>(code.data());
VkShaderModule shaderModule;
if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {
throw std::runtime_error("failed to create shader module!");
}
return shaderModule;
}
VkPipelineShaderStageCreateInfo
要实际使用着色器,我们需要通过VkPipelineShaderStagetInfo结构将它们分配给特定的管道阶段
第一步是告诉Vulkan着色器将在哪个管道阶段使用,每个可编程阶段都有一个枚举值,比如VK_SHADER_STAGE_VERTEX_BIT和VK_SHADER_STAGE_FRAGMENT_BIT
module是Shader modules
pname指明要调用的函数(入口点)
cpp
VkPipelineShaderStageCreateInfo vertShaderStageInfo{};
vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT;
vertShaderStageInfo.module = vertShaderModule;
vertShaderStageInfo.pName = "main";
VkPipelineShaderStageCreateInfo fragShaderStageInfo{};
fragShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
fragShaderStageInfo.stage = VK_SHADER_STAGE_FRAGMENT_BIT;
fragShaderStageInfo.module = fragShaderModule;
fragShaderStageInfo.pName = "main";