SwallowJoe的博客

Be a real go-getter,
NEVER SETTLE!

0%

Vulkan入门(5)-图像视图及Pipeline简述

简述

本文主要介绍VkImageView以及着色器的创建,并且我们将学习到如何编写一个渐变颜色的三角形着色器。

参考资料

  1. [SPIR-V] https://www.khronos.org/spir/
  2. [SPIR-V doc] https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html
  3. [GLSL开发手册] https://github.com/wshxbqq/GLSL-Card

一. Image views

在Render pipeline中使用VkImage, 包括在交换链中,需要创建一个VkImageView的对象。
VkImageView实际上就是图像的视图。它描述了如何访问图像以及要访问的图像部分,例如,如果它应被视为2D纹理深度纹理而没有任何mipmapping级别。
接下来我们试试为交换链中的每个图像创建一个基本VkImageView。
创建VkImageView的方式也是通过一个结构体:VkImageViewCreateInfo, 来指明细节.

1
2
3
4
5
6
7
8
9
10
typedef struct VkImageViewCreateInfo {
VkStructureType sType;
const void* pNext;
VkImageViewCreateFlags flags;
VkImage image;
VkImageViewType viewType;
VkFormat format;
VkComponentMapping components;
VkImageSubresourceRange subresourceRange;
} VkImageViewCreateInfo;

参数说明:

  • VkImage: 绑定对应图像
  • VkImageViewType: 图像视图类型
    • 一维纹理: VK_IMAGE_VIEW_TYPE_1D、VK_IMAGE_VIEW_TYPE_1D_ARRAY
    • 二维纹理: VK_IMAGE_VIEW_TYPE_2D、VK_IMAGE_VIEW_TYPE_2D_ARRAY
    • 三维纹理: VK_IMAGE_VIEW_TYPE_3D
    • 立方体贴图: VK_IMAGE_VIEW_TYPE_CUBE、VK_IMAGE_VIEW_TYPE_CUBE_ARRAY
  • VkFormat: 图像格式
  • VkComponentMapping: 图像颜色通道,即RGB和Alpha通道
  • VkImageSubresourceRange:描述了图像的目的以及应该访问图像的哪个部分

特别的,如果是3D应用程序, 那应该创建一个带有多个layer的交换链。这样可以通过访问不同的图层为每个图像创建多个图像视图,以表示左右视图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
void createImageViews() {
// 设置集合大小
swapChainImageViews.resize(swapChainImages.size());

for (size_t i = 0; i < swapChainImageViews.size(); i++) {
VkImageViewCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
createInfo.image = swapChainImages[i]; // 绑定 VkImage

// viewType和format字段指定应如何解释图像数据
// viewType参数指定图像为一维纹理,二维纹理,三维纹理或立方体贴图
createInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
// 图像格式
createInfo.format = swapChainImageFormat;
// 图像颜色通道,即RGB和Alpha通道。比如将所有通道映射到红色通道以获得单色纹理,或者将常量值0和1映射到通道。这里选择默认映射:
createInfo.components.r = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.g = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.b = VK_COMPONENT_SWIZZLE_IDENTITY;
createInfo.components.a = VK_COMPONENT_SWIZZLE_IDENTITY;

// subresourceRange字段描述了图像的目的是什么以及应该访问图像的哪个部分。
// 这里图像将用作颜色目标,没有任何mipmapping级别或多个层。
createInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
createInfo.subresourceRange.baseMipLevel = 0;
createInfo.subresourceRange.levelCount = 1;
createInfo.subresourceRange.baseArrayLayer = 0;
createInfo.subresourceRange.layerCount = 1;

// 注意,通过vkCreateXXX创建的对象,都需要我们主动去释放
if (vkCreateImageView(device, &createInfo, nullptr, &swapChainImageViews[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to create image views!");
}
}
}

void cleanup() {
// 释放交换链对应的图像视图
for (auto imageView : swapChainImageViews) {
vkDestroyImageView(device, imageView, nullptr);
}
.....
}

有了图像视图足以开始将图像用作纹理,但还不能直接用作渲染目标。需要一个间接步骤,称为帧缓冲,但首先我们必须设置图形管道。

二. 图形管道 Pipeline 简介

所谓图形管道就是一系列操作,它们将网格的顶点和纹理一直带到渲染目标中的像素。简化概述如下所示:

  1. 输入汇编程序(input assembler): 从指定的缓冲区收集原始顶点数据,也可以使用索引缓冲区重复某些元素,而不必复制顶点数据本身。
  2. 顶点着色器(vertex shader): 针对每个顶点运行,并且通常应用变换以将顶点位置从模型空间转换到屏幕空间。它还沿着管道传递每顶点数据。
  3. 曲面细分着色器(tessellation shaders): 根据特定规则细分几何体以提高网格质量。通常用于使砖墙和楼梯等表面在附近时看起来不那么平坦。
  4. 几何着色器(geometry shader): 在每个基元(三角形,直线,点)上运行,并且可以丢弃它或输出比原来更多的基元。类似于曲面细分着色器,但更灵活。但没有得到太多应用,因为大多数显卡的性能都不是很好。
  5. 光栅化阶段(rasterization stage): 将基元离散化为片段。这些是它们填充在帧缓冲区上的像素元素。在屏幕之外的片段都将被丢弃,顶点着色器输出的属性将在片段之间进行插值。由于深度测试,通常在这里也丢弃其他原始片段后面的片段。
  6. 片段着色器(fragment shader): 为存活的每个片段调用片段着色器,并确定片段写入哪些帧缓冲区以及使用哪些颜色和深度值。它可以使用来自顶点着色器的插值数据来完成此操作,其中可以包括纹理坐标和法线照明等内容。
  7. 颜色混合阶段(color blending stage): 应用操作来混合映射到帧缓冲区中的相同像素的不同片段。 碎片可以简单地相互覆盖,加起来或根据透明度进行混合。
    1. 绿色的阶段被称为固定功能阶段。这些阶段允许使用参数调整其操作,但它们的工作方式是预定义的。
    2. 橙色的阶段是可编程的,这意味着可以将代码上传到图形卡,以完全应用想要的操作。
    3. 例如,实现从纹理和光照到光线跟踪器的任何内容。这些程序同时在许多GPU内核上运行,以并行处理许多对象,如顶点和片段。,可以使用片段着色器

图片

在OpenGL和Direct3D中,可以使用glBlendFunc和OMSetBlendState等调用随意更改任何管道设置。
但Vulkan中的图形管道几乎完全不可变,因此如果要更改着色器,绑定不同的帧缓冲区或更改混合函数,则必须从头开始重新创建管道。
缺点是您必须创建许多管道,这些管道代表您要在渲染操作中使用的所有状态组合。但是,因为您将在管道中执行的所有操作都是事先知道的,所以驱动程序可以更好地优化它。

根据您的目的,某些可编程阶段是可选的。例如,如果您只是绘制简单几何体,则可以禁用曲面细分和几何体阶段。
如果您只对深度值感兴趣,则可以禁用片段着色器阶段,这对阴影贴图生成很有用。

记住创建管道需要在创建VkImageView之后

具体的创建需要依赖上述各个着色器,我们先熟悉一下这些着色器。

三. 着色器(Shader modules)

Vulkan中的着色器代码必须以字节码格式指定,而不是像GLSL和HLSL这样的人类可读语法。

  • GLSL是一种具有C风格语法的着色语言。写在其中的程序具有为每个对象调用的主函数。
  • GLSL使用全局变量来处理输入和输出,而不是使用输入参数和返回值作为输出。该语言包括许多有助于图形编程的功能,如内置向量和矩阵基元。包括交叉积,矩阵向量积和向量周围反射等操作的函数。

而Vulkan中的这种字节码格式称为SPIR-V,旨在与Vulkan和OpenCL(两种Khronos API)一起使用。
SPIR-V是一种用于图形着色器和计算内核的简单二进制中间语言。 更多信息可以参考 https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html

使用字节码格式的优点是比GPU供应商编写的将着色器代码转换为本机代码的编译器要简单的得多。
过去已经表明,使用像GLSL这样的人类可读语法,一些GPU供应商对标准的解释相当灵活。
如果碰巧使用其中一个供应商编写的不标准GPU着色器,那么由于语法错误,可能其他供应商的驱动程序会拒绝我们的代码,或者更糟糕的,由于编译器错误,着色器运行方式不同。而使用简单的字节码格式,如SPIR-V,则可以避免此类问题。

但是,这并不意味着我们需要手动编写这个字节码。 Khronos发布了自己独立于供应商的编译器,将GLSL编译为SPIR-V。
此编译器旨在验证着色器代码是否完全符合标准,并生成一个可与程序一起提供的SPIR-V二进制文件。
我们还可以将此编译器作为库包含在运行时生成SPIR-V。
这个编译器已经包含在LunarG SDK中作为glslangValidator.exe,无需额外下载。

接下来我们使用GLSL语言(详细参考: https://github.com/wshxbqq/GLSL-Card)编写着色器。

3.1 顶点着色器 Vertex Shader

矢量类型称为vec,其数字表示元素的数量。 例如,3D位置将存储在vec3中。
可以通过.x等成员访问单个组件,但也可以同时从多个组件创建新的向量。 例如,表达式vec3(1.0,2.0,3.0).xy将导致vec2。
向量的构造函数也可以采用向量对象和标量值的组合。 例如,vec3可以用vec3(vec2(1.0,2.0),3.0)构建。

顶点着色器处理每个传入的顶点。 它将其属性(如世界位置,颜色,法线和纹理坐标)作为输入。
输出是剪辑坐标中的最终位置以及需要传递到片段着色器的属性,如颜色和纹理坐标。
然后,光栅化器将这些值插入片段上以产生平滑的梯度。
剪辑坐标是来自顶点着色器的四维矢量,其随后通过将整个矢量除以其最后一个分量而变为标准化设备坐标。 这些标准化的设备坐标是齐次坐标,它将帧缓冲区映射到[-1,1]乘[-1,1]坐标系,如下所示:

图片

注意xy轴的方向,类似Android中的坐标轴,而不是OpenGL中的坐标轴方向。
接下来我们通过顶点着色器和片段着色器以在屏幕上呈现一个三角形,如下图:

图片

我们可以直接输出归一化设备坐标,方法是将它们作为顶点着色器的剪辑坐标输出,最后一个组件设置为1.
这样,将剪辑坐标转换为规范化设备坐标的划分不会改变任何东西。
通常这些坐标将存储在顶点缓冲区中,但在Vulkan中创建顶点缓冲区并用数据填充它并不简单。
如果我们直接在顶点着色器中包含坐标,那么可以这样写:

1
2
3
4
5
6
7
8
9
10
11
#version 450

vec2 positions[3] = vec2[](
vec2(0.0, -0.5),
vec2(0.5, 0.5),
vec2(-0.5, 0.5)
);

void main() {
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
}

每个顶点的生成都需要调用main函数。
内置的gl_VertexIndex变量包含当前顶点的索引,一般是顶点缓冲区的索引。在这里,它是顶点数据的硬编码数组的索引。
从着色器中的常量数组访问每个顶点的位置,并与虚拟z和w组件组合以在剪辑坐标中生成位置,内置变量gl_Position用作输出。

3.2 片段着色器 Fragment Shader

由顶点着色器的位置形成的三角形用片段填充屏幕上的区域。
在这些片段上调用片段着色器以生成帧缓冲区(或帧缓冲区)的颜色和深度。
为整个三角形输出红色的简单片段着色器如下所示:

1
2
3
4
5
6
7
8
9
#version 450
#extension GL_ARB_separate_shader_objects : enable

// 帧缓冲区的索引为0
layout(location = 0) out vec4 outColor;

void main() {
outColor = vec4(1.0, 0.0, 0.0, 1.0);
}

GLSL中的颜色是4分量矢量,其中R,G,B和α通道在[0,1]范围内。
与顶点着色器中的gl_Position不同,没有内置变量来输出当前片段的颜色。 所以必须为每个帧缓冲区指定自己的输出变量,其中layout(location = 0)修饰符指定帧缓冲区的索引。
红色将写入此outColor变量,该变量链接到索引0处的第一个(也是唯一的)帧缓冲区。

3.3 为每个顶点着色

如果我们想要实现渐变颜色的三角形,如下:

图片

这就需要我们为三个顶点中的每一个指定不同的颜色。 顶点着色器现在应该包含一个颜色的数组,就像它对位置一样.

1
2
3
4
5
6
vec3 colors[3] = vec3[](
// vec3(r,g,b)
vec3(1.0, 0.0, 0.0),
vec3(0.0, 1.0, 0.0),
vec3(0.0, 0.0, 1.0)
);

每一个vec3对应一个顶点。现在我们只需要将这些顶点颜色传递给片段着色器,这样就可以将它们的插值输出到帧缓冲区。
将颜色输出添加到顶点着色器并在main函数中写入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#version 450

vec2 positions[3] = vec2[](
vec2(0.0, -0.5),
vec2(0.5, 0.5),
vec2(-0.5, 0.5)
);

layout(location = 0) out vec3 fragColor;

void main() {
gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0);
fragColor = colors[gl_VertexIndex];
}

接下来,我们需要在片段着色器中添加匹配的输入:

1
2
3
4
5
6
7
8
9
#version 450
#extension GL_ARB_separate_shader_objects : enable

// 帧缓冲区的索引为0
layout(location = 0) in vec3 fragColor;

void main() {
outColor = vec4(fragColor, 1.0);
}

输入变量不一定必须使用相同的名称,因为我们使用location指令指定的索引讲它们链接在一起。
如上图所示,fragColor的值将自动插入三个顶点之间的片段,从而产生平滑的渐变。

3.4 编译着色器

首先在我们的工程目录下创建一个 shaders 的目录,用于保存我们的着色器。
首先是 shader.vert 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#version 450
#extension GL_ARB_separate_shader_objects : enable

// 输出为fragColor
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];
}

还有 shader.frag 文件:

1
2
3
4
5
6
7
8
9
10
#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(location = 0) in vec3 fragColor;

layout(location = 0) out vec4 outColor;

void main() {
outColor = vec4(fragColor, 1.0);
}

3.4.1 Linux平台下编译方式

使用 vulkan SDK中的glslangValidator来编译shader文件:

1
2
3
4
#/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslangValidator -V shader.vert
#/home/user/VulkanSDK/x.x.x.x/x86_64/bin/glslangValidator -V shader.frag
/home/jh/Program/vulkan/1.1.106.0/x86_64/bin/glslangValidator -V shader.vert
/home/jh/Program/vulkan/1.1.106.0/x86_64/bin/glslangValidator -V shader.frag

执行上面的脚本后,会在当前目录生成对应的: frag.spv和vert.spv文件
Vulkan_5_5.png

3.4.2 Windows平台编译方式

类似,不赘述

另外Vulkan SDK包含libshaderc库,用于从程序中将GLSL代码编译为SPIR-V。

3.5 加载着色器

加载着色器就是读取我们编译好的shader文件:frag.spv和vert.spv.
c++中读取文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <fstream>

static std::vector<char> readFile(const std::string& filename) {
// ate: 从文件末尾开始阅读
// binary: 二进制流形式
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;
}

比如读取 vert.spv:

1
auto vertShaderCode = readFile("shaders/vert.spv");

读取成功后,记得check一下文件大小是否匹配。

3.6 创建着色器模块(shader modules - VkShaderModule)

在Vulkan中使用VkShaderModule存储着色器. 使用结构体:

1
2
3
4
5
6
7
typedef struct VkShaderModuleCreateInfo {
VkStructureType sType;
const void* pNext;
VkShaderModuleCreateFlags flags;
size_t codeSize;
const uint32_t* pCode;
} VkShaderModuleCreateInfo;

封装成createShaderModule方法,方便后续调用.
其实VkShaderModule只是一个对着色器文件的封装而已。使用方法:vkCreateShaderModule

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;

// vkResult device, const *pCreateInfo, const VkAllocationCallbacks *pAllocator, VkShaderModule *pShaderModule)
if (vkCreateShaderModule(device, &createInfo, nullptr, &shaderModule) != VK_SUCCESS) {
throw std::runtime_error("failed to create shader module!");
}

return shaderModule;
}

3.7 着色器阶段创建(shader stage)

只是有 VkShaderModule,还不够。要使用着色器,还需要在创建管道(Pipeline)时,使用VkPipelineShaderStageCreateInfo结构讲其分配到特定的管道阶段。
比如在管道中填充顶点着色器: vert.spv

1
2
3
4
5
6
7
8
9
VkPipelineShaderStageCreateInfo vertShaderStageInfo = {};
vertShaderStageInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO;
vertShaderStageInfo.stage = VK_SHADER_STAGE_VERTEX_BIT; // 指明当前是顶点阶段 (3.7.1)

// 指定包含代码的着色器模块
vertShaderStageInfo.module = vertShaderModule;
// 指定要调用的着色器模块函数(称为入口点)
vertShaderStageInfo.pName = "main";

还有一个(可选的)成员pSpecializationInfo,这里不会在这里使用。它允许您指定着色器常量的值。
当使用单个着色器模块,通过为其中使用的常量指定不同的值,就可以在创建管道时配置其行为。
这比在渲染时使用变量配置着色器更有效,因为编译器可以执行优化,例如消除依赖于这些值的if语句。
默认为nullptr,struct初始化会自动执行。

3.7.1 Shader Stage

1
2
3
4
5
6
7
8
9
10
11
typedef enum VkShaderStageFlagBits {
VK_SHADER_STAGE_VERTEX_BIT = 0x00000001,
VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT = 0x00000002,
VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT = 0x00000004,
VK_SHADER_STAGE_GEOMETRY_BIT = 0x00000008,
VK_SHADER_STAGE_FRAGMENT_BIT = 0x00000010,
VK_SHADER_STAGE_COMPUTE_BIT = 0x00000020,
VK_SHADER_STAGE_ALL_GRAPHICS = 0x0000001F,
VK_SHADER_STAGE_ALL = 0x7FFFFFFF,
VK_SHADER_STAGE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkShaderStageFlagBits;
  • VK_SHADER_STAGE_VERTEX_BIT: 顶点阶段
  • VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT: 曲面细分控制阶段
  • VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT: 曲面细分评估阶段
  • VK_SHADER_STAGE_GEOMETRY_BIT: 几何阶段
  • VK_SHADER_STAGE_FRAGMENT_BIT: 片段阶段
  • VK_SHADER_STAGE_COMPUTE_BIT: 计算阶段
  • VK_SHADER_STAGE_ALL_GRAPHICS: 用作速记的位组合,用于指定上面定义的所有图形阶段(不包括计算阶段)。
  • VK_SHADER_STAGE_FLAG_BITS_MAX_ENUM: 用作速记的位组合,用于指定设备支持的所有着色器阶段,包括扩展引入的所有其他阶段。

3.7.2 为着色器指定管道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void createGraphicsPipeline() {
auto vertShaderCode = readFile("shaders/vert.spv");
auto fragShaderCode = readFile("shaders/frag.spv");

VkShaderModule vertShaderModule = createShaderModule(vertShaderCode);
VkShaderModule fragShaderModule = createShaderModule(fragShaderCode);

// 这里为管道指定着色器
VkPipelineShaderStageCreateInfo shaderStages[] = {vertShaderStageInfo, fragShaderStageInfo};

// 记得销毁shader
vkDestroyShaderModule(device, vertShaderModule, nullptr);
vkDestroyShaderModule(device, fragShaderModule, nullptr);
}

图形管道 其他部分设置在下个文章中讨论。

欢迎关注我的其它发布渠道