SwallowJoe的博客

Be a real go-getter,
NEVER SETTLE!

0%

Vulkan入门(15)-图像视图和采样器

简述

回顾上一篇章中的读取图像的步骤:

  1. 首先利用stb-image库读取图片,将其内容存储在临时缓存区VkBuffer中,注意需要开辟GPU可见内存
  2. 通过VkImageCreateInfo结构指明图像格式并通过vkCreateImage创建VkImage对象
  3. 用VkBuffer图像文件中的像素填充创建的VkImage图像对象
    1. 填充图像对象需要使用VkImageMemoryBarrier
    2. 使用vkCmdPipelineBarrier使得图像填充Barrier生效
    3. 通过vkCmdCopyBufferToImage完成图像像素从VkBuffer到VkImage的拷贝(填充)
    4. 再通过VkImageMemoryBarrier指定图像是能够从着色器中的纹理图像开始采样
  4. 创建图像视图和图像采样器
  5. 添加一个组合的图像采样器描述符来从纹理中采样颜色

在图像采样器创建之前,我们首先看看纹理图像视图,这个是在我们创建交换链的时候见过:

一. 纹理图像视图 Texture Image View

通过VkImageView类来存储纹理图像视图, 它描述了如何访问图像以及要访问的图像部分,创建VkImageView的方式也是通过一个结构体:VkImageViewCreateInfo, 来指明细节. 这部分我们在之前的交换链创建图像视图中有过接触,如果忘记了的话可以回顾一下。

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
VkImageView textureImageView;

void initVulkan() {
...
createTextureImage();
createTextureImageView();
createVertexBuffer();
...
}

VkImageView createImageView(VkImage image, VkFormat format) {
VkImageView imageView;
VkImageViewCreateInfo viewInfo = {};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = image; // 绑定 VkImage

// viewType和format字段指定应如何解释图像数据
// viewType参数指定图像为一维纹理,二维纹理,三维纹理或立方体贴图
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
// 图像格式
viewInfo.format = format;

// subresourceRange字段描述了图像的目的是什么以及应该访问图像的哪个部分。
// 这里图像将用作颜色目标,没有任何mipmapping级别或多个层。
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
viewInfo.subresourceRange.baseMipLevel = 0;
viewInfo.subresourceRange.levelCount = 1;
viewInfo.subresourceRange.baseArrayLayer = 0;
viewInfo.subresourceRange.layerCount = 1;
// 注意,通过vkCreateXXX创建的对象,不需要时要主动去释放
if (vkCreateImageView(device, &viewInfo, nullptr, &imageView) != VK_SUCCESS) {
throw std::runtime_error("failed to create image views!");
}
return imageView;
}

void createImageViews() {
// 设置集合大小
swapChainImageViews.resize(swapChainImages.size());

for (size_t i = 0; i < swapChainImageViews.size(); i++) {
swapChainImageViews[i] = createImageView(swapChainImages[i],
swapChainImageFormat);
}
}

void createTextureImageView() {
textureImageView = createImageView(textureImage,
VK_FORMAT_R8G8B8A8_UNORM);
}

// 在 vulkan 中推荐在创建的资源不需要后主动释放
void cleanup() {
// 清理交换链关联资源
cleanupSwapChain();
// 清理纹理贴图
vkDestroyImageView(device, textureImageView, nullptr);
vkDestroyImage(device, textureImage, nullptr);
vkFreeMemory(device, textureImageMemory, nullptr);
...
}

如上,纹理图像的视图创建成功了,很简单。接下来就是采样器的创建了。

二. 采样器

着色器可以直接从图像读取纹理像素,但是当将其用作纹理时,一般不会直接读取。 通常通过采样器访问纹理,采样器将应用过滤和转换以计算最终获取的颜色。

这些过滤器有助于处理过采样等问题。 考虑一个映射到几何图形的纹理,该纹理的碎片多于纹理像素。

如果只是在每个片段中使用最接近的纹理像素作为纹理坐标,那么将获得下图左边图像的结果:

图像15-1

而通过线性插值法将4个最接近的纹理像素组合在一起,那么将获得如上右图所示的更平滑的结果。 当然,您的应用程序可能具有更适合左侧风格的艺术风格要求(比如Minecraft,哈哈),但是在常规图形应用程序中,右侧风格是首选,图像越精细越好。 从纹理读取颜色时,采样器对象会自动为您应用此过滤。

抽样不足(欠采样)则是相反的问题,比如纹理像素多于片段。这将导致在以锐角采样高频图案(如棋盘纹理)时产生伪影:

图像15-2

如左图所示,纹理在远处变得模糊混乱。解决这个问题的方法是各向异性滤波,它也可以由采样器自动应用。

除了这些过滤器,采样器还可以处理转换。它决定当你试图通过它的寻址模式读取图像外的texel时会发生什么。下面的图片显示了一些可能性:

图像15-3

2.1 createTextureSampler

现在创建一个函数createTextureSampler来设置这样的采样对象。稍后我们将在着色器中使用采样器从纹理中读取颜色:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
VkImageView textureImageView;
VkSampler textureSampler;
...

void initVulkan() {
...
createTextureImage();
createTextureImageView();
createTextureSampler();
...
}

void createTextureSampler() {
// 采样器通过VkSamplerCreateInfo结构进行配置,该结构指定了应应用的所有过滤器和转换。
VkSamplerCreateInfo samplerInfo = {};
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
// magFilter和minFilter字段指定如何对放大或缩小的纹理像素进行插值。
// 放大倍数与上面描述的过采样问题有关,而缩小倍数与欠采样有关。
// 指定要应用于查找的放大滤镜为线性过滤
samplerInfo.magFilter = VK_FILTER_LINEAR;
// 指定要应用于查找的缩小过滤器为线性过滤
samplerInfo.minFilter = VK_FILTER_LINEAR;
// 指定U、V、W坐标的[0..1]范围之外的寻址模式, 指定当超出图像尺寸时,重复纹理
// 注意,轴称为U,V和W,而不是X,Y和Z。这是纹理空间坐标的约定
samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;

// anisotropyEnable为true, 采样器使用使用各向异性过滤
samplerInfo.anisotropyEnable = VK_TRUE;
samplerInfo.maxAnisotropy = 16;

// borderColor字段指定在使用边界寻址模式对图像进行采样以外时返回的颜色。
// 可以以float或int格式返回黑色,白色或透明。
samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
// 指定要用于处理图像中纹理像素的坐标系
// 为VK_FALSE,则将使用所有轴上的[0,1)范围对纹理像素进行寻址
samplerInfo.unnormalizedCoordinates = VK_FALSE;

// 如果启用了比较功能,则将首先将纹理像素与一个值进行比较,并且该比较的结果将用于过滤操作中。 主要用于阴影贴图上的百分比封闭器过滤。
samplerInfo.compareEnable = VK_FALSE;
samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;

// 所有这些字段都适用于mipmapping。以后讨论mipmapping
samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
samplerInfo.mipLodBias = 0.0f;
samplerInfo.minLod = 0.0f;
samplerInfo.maxLod = 0.0f;

// 创建采样器,注意清理
if (vkCreateSampler(device, &samplerInfo, nullptr, &textureSampler) != VK_SUCCESS) {
throw std::runtime_error("failed to create texture sampler!");
}
}

void cleanup() {
cleanupSwapChain();
// 清理采样器
vkDestroySampler(device, textureSampler, nullptr);
...
}

samplerInfo中的anisotropyEnable和maxAnisotropy这两个字段指定是否应该使用各向异性过滤。最大各向异性字段限制了可以用来计算最终颜色的texel样本的数量。数值越低,性能越好,但质量越低。目前没有任何图形硬件可以使用超过16个样本,因为超过这个值的差异就可以忽略不计了。

unnormalizedCoordinates字段指定要用于处理图像中纹理像素的坐标系。 如果此字段为VK_TRUE,则可以简单地使用[0,texWidth)和[0,texHeight)范围内的坐标。 如果为VK_FALSE,则将使用所有轴上的[0,1)范围对纹理像素进行寻址。 实际应用中几乎总是使用归一化的坐标,因为这样一来,便可以使用分辨率完全相同的不同分辨率的纹理。

请注意,采样器未在任何地方引用VkImage。 采样器是一个独特的对象,它提供了一个接口来从纹理中提取颜色。 它可以应用于所需的任何图像,无论是1D,2D还是3D。 这与许多较早的API不同,后者将纹理图像和过滤合并为一个状态。

2.2 VkSamplerCreateInfo

VkSamplerCreateInfo结构体指定了采样器对象的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct VkSamplerCreateInfo {
VkStructureType sType;
const void* pNext;
VkSamplerCreateFlags flags;
VkFilter magFilter;
VkFilter minFilter;
VkSamplerMipmapMode mipmapMode;
VkSamplerAddressMode addressModeU;
VkSamplerAddressMode addressModeV;
VkSamplerAddressMode addressModeW;
float mipLodBias;
VkBool32 anisotropyEnable;
float maxAnisotropy;
VkBool32 compareEnable;
VkCompareOp compareOp;
float minLod;
float maxLod;
VkBorderColor borderColor;
VkBool32 unnormalizedCoordinates;
} VkSamplerCreateInfo;
  1. sType是此结构的类型, VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO
  2. pNext是NULL或指向扩展特定结构的指针
  3. flag是VkSamplerCreateFlagBits的位掩码,描述了采样器的其他参数
  4. magFilter是VkFilter值,用于指定要应用于查找的放大滤镜
    1. VK_FILTER_NEAREST 指定最近的过滤
    2. VK_FILTER_LINEAR 指定线性过滤
    3. VK_FILTER_CUBIC_EXT 指定三次过滤
    4. VK_FILTER_CUBIC_IMG 指定三次过滤,同VK_FILTER_CUBIC_EXT
  5. minFilter是一个VkFilter值,用于指定要应用于查找的缩小过滤器
  6. mipmapMode是VkSamplerMipmapMode值,指定要应用于查找的mipmap过滤器
  7. addressModeU是VkSamplerAddressMode值,用于为U坐标指定[0..1]范围之外的寻址模式
    1. VK_SAMPLER_ADDRESS_MODE_REPEAT 当超出图像尺寸时,重复纹理
    2. VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT 类似于重复,但是当超出尺寸时会反转坐标以镜像图像
    3. VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE 在图像尺寸之外,获取最靠近坐标的边缘的颜色
    4. VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER 当采样超出图像尺寸时,返回纯色
    5. VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE 类似CLAMP_TO_EDGE,但使用与最近边缘相反的边缘,仅在启用samplerMirrorClampToEdge或启用[VK_KHR_sampler_mirror_clamp_to_edge]扩展名后才有效
  8. addressModeV是VkSamplerAddressMode值,用于指定V坐标的[0..1]范围之外的寻址模式
  9. addressModeW是VkSamplerAddressMode值,它为W坐标指定[0..1]范围之外的寻址模式
  10. mipLodBias是要添加到mipmap LOD(详细程度)计算中的偏差,以及由SPIR-V中的图像采样功能提供的偏差
  11. anisotropyEnable为VK_TRUE以启用各向异性过滤,如“ Texel各向异性过滤”部分所述,否则为VK_FALSE
  12. maxAnisotropy是anisotropyEnable为VK_TRUE时采样器使用的各向异性值钳位。如果anisotropyEnable为VK_FALSE,则maxAnisotropy被忽略
  13. compareEnable为VK_TRUE,以允许在查找过程中与参考值进行比较,否则为VK_FALSE
    1. 注意:如果此成员不匹配,则某些实现将默认为着色器状态
  14. compareOp是一个VkCompareOp值,它指定比较功能,以按“深度比较操作”部分所述在过滤之前将其应用于获取的数据
  15. minLod和maxLod是用于钳位计算的LOD值的值
  16. borderColor是VkBorderColor值,用于指定要使用的预定义边框颜色
  17. unnormalizedCoordinates指定要用于处理图像中纹理像素的坐标系。设置为VK_TRUE时,用于查找纹理像素的图像坐标的范围在0到x,y和z的图像尺寸的范围内。设置为VK_FALSE时,图像坐标范围为零到一。
    1. 当unnormalizedCoordinates为VK_TRUE时,在着色器中使用采样器的图像具有以下要求:
      1. viewType必须为VK_IMAGE_VIEW_TYPE_1D或VK_IMAGE_VIEW_TYPE_2D
      2. 图像视图必须具有单个图层和单个mip级别
    2. 当unnormalizedCoordinates为VK_TRUE时,使用采样器的着色器中的图像内置函数具有以下要求:
      1. 这些功能不得使用投影
      2. 这些函数不得使用偏移量

2.3 vkCreateSampler

VkSampler对象表示图像采样器的状态,实现可使用该对象读取图像数据并为着色器应用过滤和其他转换。

1
2
3
4
5
VkResult vkCreateSampler(
VkDevice device,
const VkSamplerCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkSampler* pSampler);
  1. device是创建采样器的逻辑设备
  2. pCreateInfo是指向VkSamplerCreateInfo结构的指针,该结构指定采样器对象的状态
  3. pAllocator控制主机内存分配
  4. pSampler是指向VkSampler句柄的指针,在该句柄中返回生成的采样器对象

2.4 设备功能之各向异性过滤

如果现在运行程序,则会看到如下所示的验证层消息:

图片15-4

这是因为各向异性过滤实际上是一个可选的设备特性。我们需要更新createLogicalDevice函数来请求它:

1
2
VkPhysicalDeviceFeatures deviceFeatures = {};
deviceFeatures.samplerAnisotropy = VK_TRUE;

即使现在的显卡不支持它的可能性很小,我们也应该更新isDeviceSuitable来检查它是否可用:

1
2
3
4
5
6
7
bool isDeviceSuitable(VkPhysicalDevice device) {
...
VkPhysicalDeviceFeatures supportedFeatures;
vkGetPhysicalDeviceFeatures(device, &supportedFeatures);
return indices.isComplete() && extensionsSupported &&
swapChainAdequate && supportedFeatures.samplerAnisotropy;
}

vkGetPhysicalDeviceFeatures重新调整VkPhysicalDeviceFeatures结构的用途,通过设置布尔值来指示支持哪些功能,而不是请求哪些功能。

除了强制各向异性过滤的可用性,也可以通过条件设置不使用它:

1
2
samplerInfo.anisotropyEnable = VK_FALSE;
samplerInfo.maxAnisotropy = 1;

2.5 小结

现在图像有了,接下来,我们将向着色器公开图像和采样器对象,以便将纹理绘制到正方形上并呈现出来。

三. 组合图像采样器

我们在统一缓冲区部分中了解了描述符。 现在我们看一种新型的描述符:组合图像采样器。 该描述符使着色器可以通过采样器对象访问图像资源。

我们将从修改描述符布局,描述符池和描述符集开始,以包括此类组合的图像采样器描述符。 之后,我们将向顶点添加纹理坐标,并修改片段着色器以从纹理读取颜色,而不仅仅是插入顶点颜色。

3.1 更新描述符

回到createDescriptorSetLayout函数,为组合的图像采样器描述符添加VkDescriptorSetLayoutBinding。 将其放在统一缓冲区之后的绑定中:

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
void createDescriptorSetLayout() {
VkDescriptorSetLayoutBinding uboLayoutBinding = {};
// 指定在着色器中使用的绑定
uboLayoutBinding.binding = 0;
// 描述符的类型
uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
uboLayoutBinding.descriptorCount = 1;
// 指定描述符将在顶点着色器阶段被引用
uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
// pImmutableSamplers仅与图像采样描述符有关
uboLayoutBinding.pImmutableSamplers = nullptr;

// 创建采样器描述符
VkDescriptorSetLayoutBinding samplerLayoutBinding = {};
samplerLayoutBinding.binding = 1;
samplerLayoutBinding.descriptorCount = 1;
samplerLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
samplerLayoutBinding.pImmutableSamplers = nullptr;
// 指明片段着色器阶段可以使用组合的图像采样器描述符, 那就是片段颜色确定的地方。
// 可以在顶点着色器中使用纹理采样,例如通过高度图使顶点网格动态变形。
samplerLayoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;

std::array<VkDescriptorSetLayoutBinding, 2> bindings = {uboLayoutBinding, samplerLayoutBinding};

VkDescriptorSetLayoutCreateInfo layoutInfo = {};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size());
layoutInfo.pBindings = bindings.data();

// 创建描述符集布局
if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) {
throw std::runtime_error("failed to create descriptor set layout!");
}
}

如果您现在运行带有验证层的应用程序,那么会发现描述符池无法使用此布局分配描述符集,因为它没有任何组合的图像采样器描述符。

转到createDescriptorPool函数并对其进行修改,以包括此描述符的VkDescriptorPoolSize:

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
void createDescriptorPool() {
VkDescriptorPoolSize poolSize = {};
// 我们创建的是统一缓冲的描述符
poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSize.descriptorCount = static_cast<uint32_t>(swapChainImages.size());

std::array<VkDescriptorPoolSize, 2> poolSizes = {};
// 第一个是统一缓冲区描述符
poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSizes[0].descriptorCount = static_cast<uint32_t>(swapChainImages.size());
// 第二个是纹理图像采样器描述符
poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
poolSizes[1].descriptorCount = static_cast<uint32_t>(swapChainImages.size());

VkDescriptorPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = static_cast<uint32_t>(poolSizes.size());
poolInfo.pPoolSizes = poolSizes.data();

// 除了可用的单个描述符的最大数量外,还需要指定可以分配的最大描述符集数量:与交换链图像数量一致
poolInfo.maxSets = static_cast<uint32_t>(swapChainImages.size());
// 创建描述符池
if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {
throw std::runtime_error("failed to create descriptor pool!");
}
}

最后一步是将实际图像和采样器资源绑定到描述符集中的描述符。 转到createDescriptorSets函数:

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
void createDescriptorSets() {
...

// 配置描述符
for (size_t i = 0; i < descriptorSets.size(); i++) {
...

// 绑定图像视图和采样器到描述符中
VkDescriptorImageInfo imageInfo = {};
imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

// 必须在VkDescriptorImageInfo结构中指定用于组合图像采样器结构的资源
imageInfo.imageView = textureImageView;
imageInfo.sampler = textureSampler;

std::array<VkWriteDescriptorSet, 2> descriptorWrites = {};
descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[0].dstSet = descriptorSets[i];
descriptorWrites[0].dstBinding = 0;
descriptorWrites[0].dstArrayElement = 0;
// 绑定统一缓冲区至描述符
descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrites[0].descriptorCount = 1;
descriptorWrites[0].pBufferInfo = &bufferInfo;

descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[1].dstSet = descriptorSets[i];
descriptorWrites[1].dstBinding = 1;
descriptorWrites[1].dstArrayElement = 0;
// 绑定图像视图和相应的采样器至描述符
descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
descriptorWrites[1].descriptorCount = 1;
descriptorWrites[1].pImageInfo = &imageInfo;
// 应用描述符集更新
vkUpdateDescriptorSets(device,
static_cast<uint32_t>(descriptorWrites.size()),
descriptorWrites.data(), 0, nullptr);
}
}

必须在VkDescriptorImageInfo结构中指定用于组合图像采样器结构的资源,就像在VkDescriptorBufferInfo结构中指定用于统一缓冲区描述符的缓冲区资源一样。

3.2 纹理坐标

纹理映射还有一个重要要素就是每个顶点的实际坐标。 坐标决定了图像如何实际映射到几何体:

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
struct Vertex {
glm::vec2 pos;
glm::vec3 color;
glm::vec2 texCoord;

static VkVertexInputBindingDescription getBindingDescription() {
VkVertexInputBindingDescription bindingDescription = {};
bindingDescription.binding = 0;
bindingDescription.stride = sizeof(Vertex);
bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
return bindingDescription;
}

static std::array<VkVertexInputAttributeDescription, 3> getAttributeDescriptions() {
std::array<VkVertexInputAttributeDescription, 3> attributeDescriptions = {};
// position 顶点输入位置属性描述符
attributeDescriptions[0].binding = 0;
attributeDescriptions[0].location = 0;
attributeDescriptions[0].format = VK_FORMAT_R32G32_SFLOAT;
attributeDescriptions[0].offset = offsetof(Vertex, pos);

// color 顶点输入颜色属性描述符
attributeDescriptions[1].binding = 0;
attributeDescriptions[1].location = 1;
attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT;
attributeDescriptions[1].offset = offsetof(Vertex, color);

// coordinates 顶点输入坐标属性描述符
attributeDescriptions[2].binding = 0;
attributeDescriptions[2].location = 2;
attributeDescriptions[2].format = VK_FORMAT_R32G32_SFLOAT;
attributeDescriptions[2].offset = offsetof(Vertex, texCoord);
return attributeDescriptions;
}
};

修改“顶点”结构,使其包含用于纹理坐标的vec2(texCoord)。 确保还添加了VkVertexInputAttributeDescription,以便我们可以将访问纹理坐标用作顶点着色器中的输入。 要将它们传递到片段着色器以便在正方形表面上进行插值,这是必要的。

1
2
3
4
5
6
const std::vector<Vertex> vertices = {
{{-0.5f, -0.5f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f}},
{{0.5f, -0.5f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f}},
{{0.5f, 0.5f}, {0.0f, 0.0f, 1.0f}, {0.0f, 1.0f}},
{{-0.5f, 0.5f}, {1.0f, 1.0f, 1.0f}, {1.0f, 1.0f}}
};

先使用从左上角的0、0到右下角的1、1的坐标简单地用纹理填充正方形。 随意尝试使用不同的坐标。 稍后我们看看低于0或高于1的坐标下的实际的寻址模式!

3.3 着色器

最后一步是修改着色器,以从纹理中采样颜色。 我们首先需要修改顶点着色器,以将纹理坐标传递到片段着色器:

1
2
3
4
5
6
7
8
9
10
11
12
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 2) in vec2 inTexCoord;

layout(location = 0) out vec3 fragColor;
layout(location = 1) out vec2 fragTexCoord;

void main() {
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);
fragColor = inColor;
fragTexCoord = inTexCoord;
}

就像每个顶点的颜色一样,栅格化器会将fragTexCoord值平滑地插入到正方形区域中。 我们可以通过使片段着色器将纹理坐标输出为颜色来形象化:

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

layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec2 fragTexCoord;

layout(location = 0) out vec4 outColor;

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

现在我们编译下着色器,然后运行下程序:

图片15-5

3.3.1 片段着色器中的图像采样器描述符

组合的图像采样器描述符在GLSL中由采样器统一表示。 在片段着色器中添加对它的引用:

1
2
3
4
5
6
# 对于其他类型的图像,存在等效的sampler1D和sampler3D类型。 确保在此处使用正确的绑定。 
layout(binding = 1) uniform sampler2D texSampler;

void main() {
outColor = texture(texSampler, fragTexCoord);
}

编译一下shader然后运行程序:

图像15-6

哒哒,一个旋转的贴图出现了!

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