图像视图和采样器

      +

      在本章中,我们将创建两个图形管线所需的资源,用于从图像中采样。第一个资源是我们之前在处理交换链图像时已经见过的,但第二个是新的——它涉及着色器如何从图像中读取纹素。

      纹理图像视图

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

      添加一个类成员来保存纹理图像的`VkImageView`,并创建一个新函数`createTextureImageView`来创建它:

      vk::raii::ImageView textureImageView = nullptr;
      
      ...
      
      void initVulkan() {
          ...
          createTextureImage();
          createTextureImageView();
          createVertexBuffer();
          ...
      }
      
      ...
      
      void createTextureImageView() {
      
      }

      这个函数的代码可以直接基于`createImageViews`。你只需要修改两个地方:format`和`image

      vk::ImageViewCreateInfo viewInfo({}, image, vk::ImageViewType::e2D, format, {}, { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 });

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

      vk::raii::ImageView( device, viewInfo );

      由于很多逻辑与`createImageViews`重复,你可能希望将其抽象为一个新的`createImageView`函数:

      vk::raii::ImageView createImageView(vk::raii::Image& image, vk::Format format) {
          vk::ImageViewCreateInfo viewInfo({}, image, vk::ImageViewType::e2D, format, {}, { vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1 });
          return vk::raii::ImageView( device, viewInfo );
      }

      现在`createTextureImageView`函数可以简化为:

      void createTextureImageView() {
          textureImageView = createImageView(textureImage, vk::Format::eR8G8B8A8Srgb);
      }

      而`createImageViews`可以简化为:

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

      采样器

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

      这些过滤器有助于处理诸如过采样之类的问题。考虑一个映射到几何体的纹理,其片段比纹素多。如果你简单地为每个片段取最接近的纹素,那么你会得到像第一张图这样的结果:

      texture filtering

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

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

      anisotropic filtering

      如左图所示,纹理在远处变成模糊的一团。解决这个问题的方法是https://en.wikipedia.org/wiki/Anisotropic_filtering[各向异性过滤],采样器也可以自动应用它。

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

      texture addressing

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

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

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

      vk::SamplerCreateInfo samplerInfo( {}, vk::Filter::eLinear, vk::Filter::eLinear);

      magFilter`和`minFilter`字段指定如何插值被放大或缩小的纹素。放大涉及上述的过采样问题,缩小涉及欠采样。选项是`VK_FILTER_NEAREST`和`VK_FILTER_LINEAR,对应于上图所示的模式。

      vk::SamplerCreateInfo samplerInfo( {}, vk::Filter::eLinear, vk::Filter::eLinear, vk::SamplerMipmapMode::eLinear, vk::SamplerAddressMode::eRepeat,
                                          vk::SamplerAddressMode::eRepeat);

      寻址模式可以使用`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:在采样超出图像尺寸时返回纯色。

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

      vk::PhysicalDeviceProperties properties = physicalDevice.getProperties();
              vk::SamplerCreateInfo samplerInfo( {}, vk::Filter::eLinear, vk::Filter::eLinear, vk::SamplerMipmapMode::eLinear, vk::SamplerAddressMode::eRepeat,
                                                  vk::SamplerAddressMode::eRepeat, vk::SamplerAddressMode::eRepeat, 0);

      这两个字段指定是否应使用各向异性过滤。除非性能是一个问题,否则没有理由不使用它。`maxAnisotropy`字段限制了可用于计算最终颜色的纹素样本数量。较低的值会提高性能,但降低质量。为了确定我们可以使用哪个值,我们需要像这样检索物理设备的属性:

      vk::PhysicalDeviceProperties properties = physicalDevice.getProperties();

      如果你查看`VkPhysicalDeviceProperties`结构的文档,你会发现它包含一个名为`limits`的`VkPhysicalDeviceLimits`成员。这个结构又有一个名为`maxSamplerAnisotropy`的成员,这是我们为`maxAnisotropy`指定的最大值。如果我们想要追求最高质量,我们可以直接使用该值:

      vk::PhysicalDeviceProperties properties = physicalDevice.getProperties();
      vk::SamplerCreateInfo samplerInfo( {}, vk::Filter::eLinear, vk::Filter::eLinear, vk::SamplerMipmapMode::eLinear, vk::SamplerAddressMode::eRepeat,
                                          vk::SamplerAddressMode::eRepeat, vk::SamplerAddressMode::eRepeat, 0, 1,
                                          properties.limits.maxSamplerAnisotropy, vk::False, vk::CompareOp::eAlways);

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

      samplerInfo.borderColor = vk::BorderColor::eIntOpaqueBlack;

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

      samplerInfo.unnormalizedCoordinates = vk::False;

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

      samplerInfo.compareEnable = vk::False;
      samplerInfo.compareOp = vk::CompareOp::eAlways;

      如果启用了比较函数,则纹素将首先与一个值进行比较,然后该比较的结果用于过滤操作。这主要用于https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch11.html[百分比接近过滤]在阴影贴图上。我们将在未来的章节中讨论这一点。

      samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear;
      samplerInfo.mipLodBias = 0.0f;
      samplerInfo.minLod = 0.0f;
      samplerInfo.maxLod = 0.0f;

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

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

      vk::raii::ImageView textureImageView = nullptr;
      vk::raii::Sampler textureSampler = nullptr;
      
      ...
      
      void createTextureSampler() {
          ...
      
          textureSampler = vk::raii::Sampler(device, samplerInfo);
      }

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

      各向异性设备特性

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

      validation layer anisotropy

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

      vk::PhysicalDeviceFeatures deviceFeatures;
      deviceFeatures.samplerAnisotropy = vk::True;

      尽管现代显卡不太可能不支持它,但我们还是应该更新`isDeviceSuitable`来检查它是否可用:

      bool isDeviceSuitable(VkPhysicalDevice device) {
          ...
      
          vk::PhysicalDeviceFeatures supportedFeatures = device.getPhysicalDeviceFeatures();
      
          return indices.isComplete() && extensionsSupported && swapChainAdequate && supportedFeatures.samplerAnisotropy;
      }

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

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

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

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