图像视图和采样器(Image view and sampler)

目录

图像视图和采样器(Image view and sampler)

纹理图像视图(Texture image view)

采样器(Samplers)

放大 / 缩小过滤(Magnification/Minification Filter)

寻址模式(Addressing Mode)

各向异性过滤(Anisotropic Filtering)

比较操作(Comparison Operation)

边界颜色(Border Color)

坐标系统(Coordinate System)

多级渐远纹理(Mipmapping)

创建采样器对象

各向异性过滤设备特性(Anisotropy device feature)

相关代码


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

纹理图像视图(Texture image view)

我们之前已经了解到,无论是交换链图像还是帧缓冲区(framebuffer),图像都是通过图像视图(image views)访问的,而非直接访问。因此,我们也需要为纹理图像创建这样一个图像视图。

添加一个类成员来存储纹理图像的 VkImageView,并创建一个新函数 createTextureImageView 用于创建该视图:

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

该函数的代码可以直接基于 createImageViews 编写,只需修改两个地方:format(格式)和 image(关联的图像):

vk::ImageViewCreateInfo viewInfo{ .image = image, .viewType = vk::ImageViewType::e2D, .format = format, .subresourceRange = { 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 = image, .viewType = vk::ImageViewType::e2D,
        .format = format, .subresourceRange = { 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);
    }
}

采样器(Samplers)

着色器可以直接从图像中读取纹理像素,但当图像用作纹理时,这种方式并不常见。纹理通常通过采样器(samplers)访问,采样器会应用过滤(filtering)和变换(transformations),以计算最终获取的颜色。

这些过滤器有助于解决诸如过采样(oversampling)等问题。假设将一个纹理映射到几何体上,而几何体的片段数(fragments)多于纹理像素数(texels):

  • 如果直接为每个片段的纹理坐标选取最近的纹理像素,会得到如左侧图像所示的效果(无过滤);
  • 如果通过线性插值(linear interpolation)合并 4 个最近的纹理像素,则会得到右侧图像所示的更平滑的结果(双线性过滤)。

当然,你的应用程序可能因艺术风格需求更适合左侧效果(例如《我的世界》),但在传统图形应用中,右侧效果更受青睐。采样器对象会在从纹理读取颜色时自动应用这种过滤。

欠采样(undersampling)则是相反的问题,即纹理像素数多于片段数。当以大角度采样高频图案(如棋盘格纹理)时,会产生 artifacts:

各向异性过滤(anisotropic filtering)也可以由采样器自动应用。

除了过滤,采样器还能处理纹理坐标变换。它通过寻址模式(addressing mode)定义了当尝试读取图像外的纹理像素时的行为,下图展示了几种常见的寻址模式:

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

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

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

放大 / 缩小过滤(Magnification/Minification Filter)

vk::SamplerCreateInfo samplerInfo{.magFilter = vk::Filter::eLinear, .minFilter = vk::Filter::eLinear};
  • magFilter:指定纹理被放大(oversampling 场景)时的插值方式;
  • minFilter:指定纹理被缩小(undersampling 场景)时的插值方式;
  • 可选值:
    • VK_FILTER_NEAREST:最近邻过滤(选取最近的纹理像素);
    • VK_FILTER_LINEAR:线性过滤(插值合并周围纹理像素)。

寻址模式(Addressing Mode)

vk::SamplerCreateInfo samplerInfo{.magFilter = vk::Filter::eLinear, .minFilter = vk::Filter::eLinear,  .mipmapMode = vk::SamplerMipmapMode::eLinear,
    .addressModeU = vk::SamplerAddressMode::eRepeat, .addressModeV = vk::SamplerAddressMode::eRepeat };
  • 可以通过 addressModeUaddressModeVaddressModeW 分别为 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:超出尺寸时返回一个固定的边界颜色。

在本教程中,我们不会采样图像外的纹理像素,因此寻址模式的选择影响不大。但重复模式(Repeat)是最常用的模式,可用于平铺地板、墙壁等纹理。

各向异性过滤(Anisotropic Filtering)

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();
    properties.limits.maxSamplerAnisotropy 给出了设备支持的最大各向异性值,直接使用该值可获得最佳质量。

比较操作(Comparison Operation)

samplerInfo.compareEnable = vk::False;
samplerInfo.compareOp = vk::CompareOp::eAlways;
  • 如果启用比较功能(compareEnable = VK_TRUE),纹理像素会先与一个参考值比较,比较结果将用于过滤操作;
  • 主要用于阴影贴图(shadow maps)的百分比接近过滤(percentage-closer filtering),后续章节会详细介绍;
  • 本教程中禁用该功能。

边界颜色(Border Color)

samplerInfo.borderColor = vk::BorderColor::eIntOpaqueBlack;
  • 当使用 VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER 寻址模式时,指定超出图像尺寸采样时返回的颜色;
  • 支持的颜色包括黑色、白色、透明色(分为浮点型和整型格式),无法指定自定义颜色。

坐标系统(Coordinate System)

samplerInfo.unnormalizedCoordinates = vk::False;
  • VK_TRUE:使用非归一化坐标,纹理像素地址范围为 [0, texWidth)[0, texHeight)
  • VK_FALSE:使用归一化坐标,纹理像素地址范围为 [0, 1)(所有轴);
  • 实际应用中几乎都使用归一化坐标,因为这样可以用完全相同的坐标适配不同分辨率的纹理。

多级渐远纹理(Mipmapping)

samplerInfo.mipmapMode = vk::SamplerMipmapMode::eLinear;
samplerInfo.mipLodBias = 0.0f;
samplerInfo.minLod = 0.0f;
samplerInfo.maxLod = 0.0f;
  • 这些字段均与多级渐远纹理相关(后续章节会详细介绍),多级渐远纹理是另一种可应用的过滤方式;
  • 目前暂不使用多级渐远纹理,保持默认值即可。

创建采样器对象

采样器的功能已完全定义,添加一个类成员存储采样器对象的句柄,并调用 vkCreateSampler 创建采样器:

vk::raii::ImageView textureImageView = nullptr;
vk::raii::Sampler textureSampler = nullptr;
...
void 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,
        .borderColor = vk::BorderColor::eIntOpaqueBlack,
        .unnormalizedCoordinates = vk::False,
        .mipLodBias = 0.0f,
        .minLod = 0.0f,
        .maxLod = 0.0f
    };
    textureSampler = vk::raii::Sampler(device, samplerInfo);
}

注意:采样器对象并未引用任何 VkImage。采样器是一个独立的对象,提供了从纹理中提取颜色的接口,可应用于任何 1D、2D 或 3D 图像。这与许多旧版 API 不同,旧版 API 会将纹理图像和过滤规则合并为单个状态。

各向异性过滤设备特性(Anisotropy device feature)

如果现在运行程序,会看到如下验证层错误信息:

这是因为各向异性过滤实际上是一个可选的设备特性,我们需要在 createLogicalDevice 函数中显式请求该特性:

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

此外,尽管现代显卡几乎都支持该特性,但仍建议在 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;

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

C++ code / slang shader / GLSL Vertex shader / GLSL Fragment shader