本文主要介绍VkImageView以及着色器的创建,并且我们将学习到如何编写一个渐变颜色的三角形着色器。
在Render pipeline中使用VkImage, 包括在交换链中,需要创建一个VkImageView的对象。
VkImageView实际上就是图像的视图。它描述了如何访问图像以及要访问的图像部分,例如,如果它应被视为2D纹理深度纹理而没有任何mipmapping级别。
接下来我们试试为交换链中的每个图像创建一个基本VkImageView。
创建VkImageView的方式也是通过一个结构体:VkImageViewCreateInfo, 来指明细节.
typedef struct VkImageViewCreateInfo {
VkStructureType sType;
const void* pNext;
VkImageViewCreateFlags flags;
VkImage image;
VkImageViewType viewType;
VkFormat format;
VkComponentMapping components;
VkImageSubresourceRange subresourceRange;
} VkImageViewCreateInfo;
参数说明:
特别的,如果是3D应用程序, 那应该创建一个带有多个layer的交换链。这样可以通过访问不同的图层为每个图像创建多个图像视图,以表示左右视图。
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);
}
.....
}
有了图像视图足以开始将图像用作纹理,但还不能直接用作渲染目标。需要一个间接步骤,称为帧缓冲,但首先我们必须设置图形管道。
所谓图形管道就是一系列操作,它们将网格的顶点和纹理一直带到渲染目标中的像素。简化概述如下所示:
在OpenGL和Direct3D中,可以使用glBlendFunc和OMSetBlendState等调用随意更改任何管道设置。
但Vulkan中的图形管道几乎完全不可变,因此如果要更改着色器,绑定不同的帧缓冲区或更改混合函数,则必须从头开始重新创建管道。
缺点是您必须创建许多管道,这些管道代表您要在渲染操作中使用的所有状态组合。但是,因为您将在管道中执行的所有操作都是事先知道的,所以驱动程序可以更好地优化它。
根据您的目的,某些可编程阶段是可选的。例如,如果您只是绘制简单几何体,则可以禁用曲面细分和几何体阶段。
如果您只对深度值感兴趣,则可以禁用片段着色器阶段,这对阴影贴图生成很有用。
记住创建管道需要在创建VkImageView之后。
具体的创建需要依赖上述各个着色器,我们先熟悉一下这些着色器。
Vulkan中的着色器代码必须以字节码格式指定,而不是像GLSL和HLSL这样的人类可读语法。
而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)编写着色器。
矢量类型称为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中创建顶点缓冲区并用数据填充它并不简单。
如果我们直接在顶点着色器中包含坐标,那么可以这样写:
#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用作输出。
由顶点着色器的位置形成的三角形用片段填充屏幕上的区域。
在这些片段上调用片段着色器以生成帧缓冲区(或帧缓冲区)的颜色和深度。
为整个三角形输出红色的简单片段着色器如下所示:
#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处的第一个(也是唯一的)帧缓冲区。
如果我们想要实现渐变颜色的三角形,如下:
这就需要我们为三个顶点中的每一个指定不同的颜色。 顶点着色器现在应该包含一个颜色的数组,就像它对位置一样.
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函数中写入:
#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];
}
接下来,我们需要在片段着色器中添加匹配的输入:
#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的值将自动插入三个顶点之间的片段,从而产生平滑的渐变。
首先在我们的工程目录下创建一个 shaders 的目录,用于保存我们的着色器。
首先是 shader.vert 文件:
#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 文件:
#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);
}
使用 vulkan SDK中的glslangValidator来编译shader文件:
#/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 SDK包含libshaderc库,用于从程序中将GLSL代码编译为SPIR-V。
加载着色器就是读取我们编译好的shader文件:frag.spv和vert.spv.
c++中读取文件如下:
#include
static std::vector 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 buffer(fileSize);
file.seekg(0);
file.read(buffer.data(), fileSize);
file.close();
return buffer;
}
比如读取 vert.spv:
auto vertShaderCode = readFile("shaders/vert.spv");
读取成功后,记得check一下文件大小是否匹配。
在Vulkan中使用VkShaderModule存储着色器. 使用结构体:
typedef struct VkShaderModuleCreateInfo {
VkStructureType sType;
const void* pNext;
VkShaderModuleCreateFlags flags;
size_t codeSize;
const uint32_t* pCode;
} VkShaderModuleCreateInfo;
封装成createShaderModule方法,方便后续调用.
其实VkShaderModule只是一个对着色器文件的封装而已。使用方法:vkCreateShaderModule
VkShaderModule createShaderModule(const std::vector& code) {
VkShaderModuleCreateInfo createInfo = {};
createInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
createInfo.codeSize = code.size();
createInfo.pCode = reinterpret_cast(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;
}
只是有 VkShaderModule,还不够。要使用着色器,还需要在创建管道(Pipeline)时,使用VkPipelineShaderStageCreateInfo结构讲其分配到特定的管道阶段。
比如在管道中填充顶点着色器: vert.spv
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初始化会自动执行。
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;
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);
}