图像视图和采样器
在本章节中,我们将创建另外两个资源,这些资源是图形管线采样图像所必需的。第一个资源是我们在处理交换链图像时已经见过的,但第二个资源是新的 - 它关系到着色器如何从图像中读取纹素。
纹理图像视图
我们之前在交换链图像和帧缓冲中已经看到,图像是通过图像视图而不是直接访问的。 我们还需要为纹理图像创建这样一个图像视图。
添加一个类成员来保存纹理图像的 VkImageView
,并创建一个新的函数 createTextureImageView
,我们将在其中创建它
VkImageView textureImageView;
...
void initVulkan() {
...
createTextureImage();
createTextureImageView();
createVertexBuffer();
...
}
...
void createTextureImageView() {
}
此函数的代码可以直接基于 createImageViews
。 你只需要做的两个更改是 format
和 image
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 texture 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);
采样器
着色器可以直接从图像中读取纹素,但这在将图像用作纹理时不是很常见。 纹理通常通过采样器访问,采样器将应用过滤和变换来计算检索到的最终颜色。
这些过滤器有助于处理诸如过采样之类的问题。 考虑一个纹理,它被映射到具有比纹素更多的片段的几何体。 如果你只是为每个片段中的纹理坐标获取最近的纹素,那么你将得到像第一张图像这样的结果
如果你通过线性插值组合 4 个最近的纹素,那么你将获得更平滑的结果,如右图所示。 当然,你的应用程序可能有更符合左侧风格的艺术风格要求(想想 Minecraft),但在传统的图形应用程序中,右侧风格是首选。 采样器对象在从纹理读取颜色时会自动应用此过滤。
欠采样是相反的问题,你有比片段更多的纹素。 当以锐角采样高频图案(如棋盘纹理)时,这将导致伪影
如左图所示,纹理在远处变成模糊的混乱。 解决这个问题的方法是 各向异性过滤,它也可以由采样器自动应用。
除了这些过滤器之外,采样器还可以处理变换。 它通过其寻址模式确定当你尝试读取图像外部的纹素时会发生什么。 下图显示了一些可能性
我们现在将创建一个函数 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;
magFilter
和 minFilter
字段指定如何插值放大或缩小的纹素。 放大涉及上面描述的过采样问题,缩小涉及欠采样。 选择是 VK_FILTER_NEAREST
和 VK_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
:类似于 clamp to edge,但使用与最接近边缘相反的边缘。 -
VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER
:当采样超出图像尺寸时返回纯色。
在这里使用哪种寻址模式并不重要,因为在本教程中我们不会在图像外部进行采样。 但是,重复模式可能是最常见的模式,因为它可用于平铺纹理,如地板和墙壁。
samplerInfo.anisotropyEnable = VK_TRUE;
samplerInfo.maxAnisotropy = ???;
这两个字段指定是否应使用各向异性过滤。 除非性能是一个问题,否则没有理由不使用它。 maxAnisotropy
字段限制了可用于计算最终颜色的纹素样本数量。 较低的值会导致更好的性能,但质量结果较低。 为了弄清楚我们可以使用的值,我们需要检索物理设备的属性,如下所示
VkPhysicalDeviceProperties properties{};
vkGetPhysicalDeviceProperties(physicalDevice, &properties);
如果你查看 VkPhysicalDeviceProperties
结构的文档,你会看到它包含一个名为 limits
的 VkPhysicalDeviceLimits
成员。 这个结构反过来又有一个名为 maxSamplerAnisotropy
的成员,这是我们可以为 maxAnisotropy
指定的最大值。 如果我们想要获得最大质量,我们可以直接使用该值
samplerInfo.maxAnisotropy = properties.limits.maxSamplerAnisotropy;
你可以选择在程序开始时查询属性,并将它们传递给需要它们的函数,或者在 createTextureSampler
函数本身中查询它们。
samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
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_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 不同,后者将纹理图像和过滤组合到一个状态中。
在程序结束时,当我们不再访问图像时,销毁采样器
void cleanup() {
cleanupSwapChain();
vkDestroySampler(device, textureSampler, nullptr);
vkDestroyImageView(device, textureImageView, nullptr);
...
}
各向异性过滤设备特性
如果你现在运行你的程序,你将看到这样的验证层消息
那是因为各向异性过滤实际上是一个可选的设备特性。 我们需要更新 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;
在下一章中,我们将向着色器公开图像和采样器对象,以将纹理绘制到正方形上。