图像视图和采样器
在本章中,我们将创建两个图形管线所需的资源,用于从图像中采样。第一个资源是我们之前在处理交换链图像时已经见过的,但第二个是新的——它涉及着色器如何从图像中读取纹素。
纹理图像视图
我们之前已经看到,交换链图像和帧缓冲是通过图像视图而不是直接访问的。我们还需要为纹理图像创建这样的图像视图。
添加一个类成员来保存纹理图像的`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);
}
}
采样器
着色器可以直接从图像中读取纹素,但当它们用作纹理时,这并不常见。纹理通常通过采样器访问,采样器会应用过滤和变换来计算最终检索的颜色。
这些过滤器有助于处理诸如过采样之类的问题。考虑一个映射到几何体的纹理,其片段比纹素多。如果你简单地为每个片段取最接近的纹素,那么你会得到像第一张图这样的结果:
如果你通过线性插值组合4个最接近的纹素,那么你会得到像右边这样更平滑的结果。当然,你的应用程序可能有更适合左边风格的艺术要求(想想Minecraft),但在传统图形应用程序中,右边是首选。采样器对象在从纹理读取颜色时会自动应用这种过滤。
欠采样是相反的问题,即纹素比片段多。当以锐角采样高频图案(如棋盘纹理)时,这会导致伪影:
如左图所示,纹理在远处变成模糊的一团。解决这个问题的方法是https://en.wikipedia.org/wiki/Anisotropic_filtering[各向异性过滤],采样器也可以自动应用它。
除了这些过滤器,采样器还可以处理变换。它通过其_寻址模式_确定当你尝试读取图像之外的纹素时会发生什么。下图展示了一些可能性:
我们现在将创建一个函数`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不同,后者将纹理图像和过滤组合为单一状态。
各向异性设备特性
如果你现在运行你的程序,你会看到类似这样的验证层消息:
这是因为各向异性过滤实际上是一个可选的设备特性。我们需要更新`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;
在下一章中,我们将向着色器暴露图像和采样器对象,以将纹理绘制到正方形上。