图像视图和采样器

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

纹理图像视图

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

添加一个类成员来保存纹理图像的 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 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);

采样器

着色器可以直接从图像读取纹素,但当它们用作纹理时,这种情况并不常见。纹理通常通过采样器访问,采样器将应用过滤和转换来计算检索到的最终颜色。

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

texture filtering

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

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

anisotropic filtering

如左图所示,纹理在远处变成模糊的一团。对此的解决方案是各向异性过滤,它也可以由采样器自动应用。

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

texture addressing

我们现在将创建一个函数 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:类似于夹紧到边缘,但改为使用与最近边缘相反的边缘。

  • 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 字段指定当使用钳位到边界寻址模式采样超出图像范围时返回的颜色。可以返回黑色、白色或透明色,可以是浮点或整数格式。你不能指定任意颜色。

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 不同,这些 API 将纹理图像和过滤组合到单个状态中。

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

void cleanup() {
    cleanupSwapChain();

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

    ...
}

各向异性设备特性

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

validation layer anisotropy

这是因为各向异性过滤实际上是一个可选的设备特性。我们需要更新 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;

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