在本章节中,我们将创建另外两个资源,这些资源是图形管线采样图像所必需的。第一个资源是我们在处理交换链图像时已经见过的,但第二个资源是新的 - 它关系到着色器如何从图像中读取纹素。

纹理图像视图

我们之前在交换链图像和帧缓冲中已经看到,图像是通过图像视图而不是直接访问的。 我们还需要为纹理图像创建这样一个图像视图。

添加一个类成员来保存纹理图像的 VkImageView,并创建一个新的函数 createTextureImageView,我们将在其中创建它

VkImageView textureImageView;

...

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

...

void createTextureImageView() {

}

此函数的代码可以直接基于 createImageViews。 你只需要做的两个更改是 formatimage

VkImageViewCreateInfo viewInfo{};
viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
viewInfo.image = textureImage;
viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
viewInfo.format = VK_FORMAT_R8G8B8A8_SRGB;
viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
viewInfo.subresourceRange.baseMipLevel = 0;
viewInfo.subresourceRange.levelCount = 1;
viewInfo.subresourceRange.baseArrayLayer = 0;
viewInfo.subresourceRange.layerCount = 1;

我省略了显式的 viewInfo.components 初始化,因为 VK_COMPONENT_SWIZZLE_IDENTITY 无论如何都被定义为 0。 通过调用 vkCreateImageView 完成图像视图的创建

if (vkCreateImageView(device, &viewInfo, nullptr, &textureImageView) != VK_SUCCESS) {
    throw std::runtime_error("failed to create texture image view!");
}

由于 createImageViews 中有如此多的逻辑是重复的,你可能希望将其抽象为一个新的 createImageView 函数

VkImageView createImageView(VkImage image, VkFormat format) {
    VkImageViewCreateInfo viewInfo{};
    viewInfo.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
    viewInfo.image = image;
    viewInfo.viewType = VK_IMAGE_VIEW_TYPE_2D;
    viewInfo.format = format;
    viewInfo.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
    viewInfo.subresourceRange.baseMipLevel = 0;
    viewInfo.subresourceRange.levelCount = 1;
    viewInfo.subresourceRange.baseArrayLayer = 0;
    viewInfo.subresourceRange.layerCount = 1;

    VkImageView imageView;
    if (vkCreateImageView(device, &viewInfo, nullptr, &imageView) != VK_SUCCESS) {
        throw std::runtime_error("failed to create texture image view!");
    }

    return imageView;
}

createTextureImageView 函数现在可以简化为

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

createImageViews 可以简化为

void createImageViews() {
    swapChainImageViews.resize(swapChainImages.size());

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

确保在程序结束时,在销毁图像本身之前,销毁图像视图

void cleanup() {
    cleanupSwapChain();

    vkDestroyImageView(device, textureImageView, nullptr);

    vkDestroyImage(device, textureImage, nullptr);
    vkFreeMemory(device, textureImageMemory, nullptr);

采样器

着色器可以直接从图像中读取纹素,但这在将图像用作纹理时不是很常见。 纹理通常通过采样器访问,采样器将应用过滤和变换来计算检索到的最终颜色。

这些过滤器有助于处理诸如过采样之类的问题。 考虑一个纹理,它被映射到具有比纹素更多的片段的几何体。 如果你只是为每个片段中的纹理坐标获取最近的纹素,那么你将得到像第一张图像这样的结果

如果你通过线性插值组合 4 个最近的纹素,那么你将获得更平滑的结果,如右图所示。 当然,你的应用程序可能有更符合左侧风格的艺术风格要求(想想 Minecraft),但在传统的图形应用程序中,右侧风格是首选。 采样器对象在从纹理读取颜色时会自动应用此过滤。

欠采样是相反的问题,你有比片段更多的纹素。 当以锐角采样高频图案(如棋盘纹理)时,这将导致伪影

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

除了这些过滤器之外,采样器还可以处理变换。 它通过其寻址模式确定当你尝试读取图像外部的纹素时会发生什么。 下图显示了一些可能性

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

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

...

void createTextureSampler() {

}

采样器通过 VkSamplerCreateInfo 结构配置,该结构指定了它应应用的所有过滤器和变换。

VkSamplerCreateInfo samplerInfo{};
samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
samplerInfo.magFilter = VK_FILTER_LINEAR;
samplerInfo.minFilter = VK_FILTER_LINEAR;

magFilterminFilter 字段指定如何插值放大或缩小的纹素。 放大涉及上面描述的过采样问题,缩小涉及欠采样。 选择是 VK_FILTER_NEARESTVK_FILTER_LINEAR,对应于上面图像中演示的模式。

samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;

可以使用 addressMode 字段按轴指定寻址模式。 可用值在下面列出。 其中大多数在上面的图像中演示。 请注意,轴称为 U、V 和 W 而不是 X、Y 和 Z。 这是纹理空间坐标的约定。

  • VK_SAMPLER_ADDRESS_MODE_REPEAT:当超出图像尺寸时重复纹理。
  • VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT:类似于重复,但在超出尺寸时反转坐标以镜像图像。
  • VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE:获取最接近超出图像尺寸的坐标的边缘颜色。
  • VK_SAMPLER_ADDRESS_MODE_MIRROR_CLAMP_TO_EDGE:类似于 clamp to edge,但使用与最接近边缘相反的边缘。
  • VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER:当采样超出图像尺寸时返回纯色。

在这里使用哪种寻址模式并不重要,因为在本教程中我们不会在图像外部进行采样。 但是,重复模式可能是最常见的模式,因为它可用于平铺纹理,如地板和墙壁。

samplerInfo.anisotropyEnable = VK_TRUE;
samplerInfo.maxAnisotropy = ???;

这两个字段指定是否应使用各向异性过滤。 除非性能是一个问题,否则没有理由不使用它。 maxAnisotropy 字段限制了可用于计算最终颜色的纹素样本数量。 较低的值会导致更好的性能,但质量结果较低。 为了弄清楚我们可以使用的值,我们需要检索物理设备的属性,如下所示

VkPhysicalDeviceProperties properties{};
vkGetPhysicalDeviceProperties(physicalDevice, &properties);

如果你查看 VkPhysicalDeviceProperties 结构的文档,你会看到它包含一个名为 limitsVkPhysicalDeviceLimits 成员。 这个结构反过来又有一个名为 maxSamplerAnisotropy 的成员,这是我们可以为 maxAnisotropy 指定的最大值。 如果我们想要获得最大质量,我们可以直接使用该值

samplerInfo.maxAnisotropy = properties.limits.maxSamplerAnisotropy;

你可以选择在程序开始时查询属性,并将它们传递给需要它们的函数,或者在 createTextureSampler 函数本身中查询它们。

samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;

borderColor 字段指定在使用 clamp to border 寻址模式采样超出图像的颜色时返回的颜色。 可以以浮点或整数格式返回黑色、白色或透明色。 你不能指定任意颜色。

samplerInfo.unnormalizedCoordinates = VK_FALSE;

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

samplerInfo.compareEnable = VK_FALSE;
samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;

如果启用了比较函数,则纹素将首先与一个值进行比较,并且该比较的结果用于过滤操作。 这主要用于阴影贴图上的 百分比接近过滤。 我们将在以后的章节中讨论这一点。

samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
samplerInfo.mipLodBias = 0.0f;
samplerInfo.minLod = 0.0f;
samplerInfo.maxLod = 0.0f;

所有这些字段都适用于 mipmapping。 我们将在 后面的章节 中讨论 mipmapping,但基本上它是可以应用的另一种类型的过滤器。

采样器的功能现已完全定义。 添加一个类成员来保存采样器对象的句柄,并使用 vkCreateSampler 创建采样器

VkImageView textureImageView;
VkSampler textureSampler;

...

void createTextureSampler() {
    ...

    if (vkCreateSampler(device, &samplerInfo, nullptr, &textureSampler) != VK_SUCCESS) {
        throw std::runtime_error("failed to create texture sampler!");
    }
}

请注意,采样器在任何地方都没有引用 VkImage。 采样器是一个独特的对象,它提供了一个从纹理中提取颜色的接口。 它可以应用于你想要的任何图像,无论是 1D、2D 还是 3D。 这与许多旧的 API 不同,后者将纹理图像和过滤组合到一个状态中。

在程序结束时,当我们不再访问图像时,销毁采样器

void cleanup() {
    cleanupSwapChain();

    vkDestroySampler(device, textureSampler, nullptr);
    vkDestroyImageView(device, textureImageView, nullptr);

    ...
}

各向异性过滤设备特性

如果你现在运行你的程序,你将看到这样的验证层消息

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

VkPhysicalDeviceFeatures deviceFeatures{};
deviceFeatures.samplerAnisotropy = VK_TRUE;

即使现代显卡不太可能不支持它,我们也应该更新 isDeviceSuitable 以检查它是否可用

bool isDeviceSuitable(VkPhysicalDevice device) {
    ...

    VkPhysicalDeviceFeatures supportedFeatures;
    vkGetPhysicalDeviceFeatures(device, &supportedFeatures);

    return indices.isComplete() && extensionsSupported && swapChainAdequate && supportedFeatures.samplerAnisotropy;
}

vkGetPhysicalDeviceFeatures 重新利用 VkPhysicalDeviceFeatures 结构来指示支持的特性,而不是通过设置布尔值请求的特性。

除了强制各向异性过滤的可用性之外,也可以通过有条件地设置来简单地不使用它

samplerInfo.anisotropyEnable = VK_FALSE;
samplerInfo.maxAnisotropy = 1.0f;

在下一章中,我们将向着色器公开图像和采样器对象,以将纹理绘制到正方形上。

C++ 代码 / 顶点着色器 / 片段着色器