狸村 Mystic Web · 狸村
目录

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

纹理图像视图

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

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

vk::raii::ImageView textureImageView = nullptr;

...

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

...

void createTextureImageView()
{
}

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

vk::ImageViewCreateInfo viewInfo{
    .image            = image,
    .viewType         = vk::ImageViewType::e2D,
    .format           = format,
    .subresourceRange = {.aspectMask = vk::ImageAspectFlagBits::eColor, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1}};

我省略了显式的 viewInfo.components 初始化,因为它默认初始化为 vk::ComponentSwizzle::eIdentity。 通过调用 vk::raii::ImageView 构造函数完成图像视图的创建:

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

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

vk::raii::ImageView createImageView(vk::Image const &image, vk::Format format)
{
  vk::ImageViewCreateInfo viewInfo{
      .image            = image,
      .viewType         = vk::ImageViewType::e2D,
      .format           = format,
      .subresourceRange = {.aspectMask = vk::ImageAspectFlagBits::eColor, .baseMipLevel = 0, .levelCount = 1, .baseArrayLayer = 0, .layerCount = 1}};
  return vk::raii::ImageView(device, viewInfo);
}

现在 createTextureImageView 函数可以简化为:

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

createImageViews 可以简化为:

void createImageViews()
{
  assert(swapChainImageViews.empty());

  swapChainImageViews.reserve(swapChainImages.size());
  for ( auto &image: swapChainImages )
  {
    swapChainImageViews.emplace_back(createImageView(image, swapChainSurfaceFormat.format));
  }
}

采样器

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

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

texture filtering

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

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

anisotropic filtering

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

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

texture addressing

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

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

...

void createTextureSampler()
{
}

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

vk::SamplerCreateInfo samplerInfo{.magFilter = vk::Filter::eLinear, .minFilter = vk::Filter::eLinear};

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

vk::SamplerCreateInfo samplerInfo{.magFilter    = vk::Filter::eLinear,
                                  .minFilter    = vk::Filter::eLinear,
                                  .mipmapMode   = vk::SamplerMipmapMode::eLinear,
                                  .addressModeU = vk::SamplerAddressMode::eRepeat,
                                  .addressModeV = vk::SamplerAddressMode::eRepeat,
                                  .addressModeW = vk::SamplerAddressMode::eRepeat};

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

  • vk::SamplerAddressMode::eRepeat:超出图像尺寸时重复纹理。

  • vk::SamplerAddressMode::eMirroredRepeat:类似于重复,但超出尺寸时会反转坐标以镜像图像。

  • vk::SamplerAddressMode::eClampToEdge:取最接近超出图像尺寸坐标的边缘颜色。

  • vk::SamplerAddressMode::eMirrorClampToEdge:类似于 clamp to edge,但使用最接近边缘的相反边缘。

  • vk::SamplerAddressMode::eClampToBorder:在超出图像尺寸采样时返回固定颜色。

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

vk::PhysicalDeviceProperties properties = physicalDevice.getProperties();
vk::SamplerCreateInfo        samplerInfo{.magFilter        = vk::Filter::eLinear,
                                         .minFilter        = vk::Filter::eLinear,
                                         .mipmapMode       = vk::SamplerMipmapMode::eLinear,
                                         .addressModeU     = vk::SamplerAddressMode::eRepeat,
                                         .addressModeV     = vk::SamplerAddressMode::eRepeat,
                                         .addressModeW     = vk::SamplerAddressMode::eRepeat,
                                         .anisotropyEnable = vk::True,
                                         .maxAnisotropy    = properties.limits.maxSamplerAnisotropy};

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

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

如果你查看 vk::PhysicalDeviceProperties 结构的文档,你会看到它包含一个名为 limitsvk::PhysicalDeviceLimits 成员。 这个结构体又有一个名为 maxSamplerAnisotropy 的成员,这是我们可以为 maxAnisotropy 指定的最大值。 如果我们想要获得最高质量,我们可以直接使用该值。 你可以在程序开始时查询属性并将其传递给需要它们的函数,或者在 createTextureSampler 函数本身中查询。

vk::PhysicalDeviceProperties properties = physicalDevice.getProperties();
vk::SamplerCreateInfo        samplerInfo{.magFilter        = vk::Filter::eLinear,
                                         .minFilter        = vk::Filter::eLinear,
                                         .mipmapMode       = vk::SamplerMipmapMode::eLinear,
                                         .addressModeU     = vk::SamplerAddressMode::eRepeat,
                                         .addressModeV     = vk::SamplerAddressMode::eRepeat,
                                         .addressModeW     = vk::SamplerAddressMode::eRepeat,
                                         .anisotropyEnable = vk::True,
                                         .maxAnisotropy    = properties.limits.maxSamplerAnisotropy,
                                         .compareEnable    = vk::False,
                                         .compareOp        = vk::CompareOp::eAlways};
samplerInfo.borderColor = vk::BorderColor::eIntOpaqueBlack;

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::CompareOp::eAlways;

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

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

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

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

vk::raii::ImageView    textureImageView   = nullptr;
vk::raii::Sampler      textureSampler     = nullptr;

...

void createTextureSampler()
{
    ...

    textureSampler = vk::raii::Sampler(device, samplerInfo);
}

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

各向异性设备功能

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

validation layer anisotropy

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

vk::StructureChain<vk::PhysicalDeviceFeatures2, vk::PhysicalDeviceVulkan13Features, vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT> featureChain = {
    {.features = {.samplerAnisotropy = true } },            // vk::PhysicalDeviceFeatures2
    {.synchronization2 = true, .dynamicRendering = true },  // vk::PhysicalDeviceVulkan13Features
    {.extendedDynamicState = true }                         // vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT
};

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

bool isDeviceSuitable(VkPhysicalDevice device)
{
    ...

		auto features                 = physicalDevice.template getFeatures2<vk::PhysicalDeviceFeatures2,
		                                                                     vk::PhysicalDeviceVulkan13Features,
		                                                                     vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT>();
		bool supportsRequiredFeatures = features.template get<vk::PhysicalDeviceFeatures2>().features.samplerAnisotropy &&
		                                features.template get<vk::PhysicalDeviceVulkan13Features>().dynamicRendering &&
		                                features.template get<vk::PhysicalDeviceVulkan13Features>().synchronization2 &&
		                                features.template get<vk::PhysicalDeviceExtendedDynamicStateFeaturesEXT>().extendedDynamicState;

		// 如果物理设备满足所有标准则返回 true
		return supportsVulkan1_3 && supportsGraphics && supportsAllRequiredExtensions && supportsRequiredFeatures;
}

vk::raii::PhysicalDevice::getFeatures2 重新利用了 vk::PhysicalDeviceFeatures 结构体,通过设置布尔值来指示支持哪些功能,而不是请求哪些功能。

与其强制要求各向异性过滤的可用性,也可以通过条件设置来简单地不使用它:

samplerInfo.anisotropyEnable = vk::False;
samplerInfo.maxAnisotropy = 1.0f;

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

Vulkan 是 Khronos Group Inc. 的注册商标

教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。