Bootstrap

VulkanTutorial(8·Shader modules)

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索引一致就行)

#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的值将自动为三个顶点之间的片段插值,从而产生平滑的颜色渐变

#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的库

……/glslc.exe …….vert -o vert.spv
……/glslc.exe …….frag -o frag.spv
pause

加载SPIR-V到程序中

在是时候将它们加载到我们的程序中,以便在某个时候将它们插入graphics pipeline 

编写一个简单的readFile函数来从文件中加载二进制数据

对于输入流对象,我们指定两个标志

  • ate:从文件末尾开始阅读(我们可以使用读取位置来确定文件的tellg()大小并分配缓冲区)
  • binary:将文件读取为二进制文件(避免文本转换)

将读取的内容返回到由std::vector管理的字节数组中

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所有指令后写

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指明要调用的函数(入口点)

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";
;