vk::raii::ImageView textureImageView = nullptr;
...
void initVulkan()
{
...
createTextureImage();
createTextureImageView();
createVertexBuffer();
...
}
...
void createTextureImageView()
{
}
在本章中,我们将创建图形管线采样图像所需的另外两个资源。 第一个资源是我们之前在处理交换链图像时已经见过的,但第二个是新的——它与着色器如何从图像中读取纹素有关。
我们之前在交换链图像和帧缓冲中已经看到,图像是通过图像视图而不是直接访问的。 我们也需要为纹理图像创建这样一个图像视图。
添加一个类成员来保存纹理图像的 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 = {.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));
}
}
着色器可以直接从图像中读取纹素,但当它们用作纹理时,这并不常见。 纹理通常通过采样器访问,采样器将应用过滤和变换来计算最终检索到的颜色。
这些过滤器有助于处理诸如过采样之类的问题。 考虑一个纹理映射到几何体上,其片段数多于纹素数。 如果你简单地为每个片段的纹理坐标取最近的纹素,那么你会得到类似第一张图片的结果:
如果你通过线性插值组合 4 个最近的纹素,那么你会得到更平滑的结果,如右侧所示。 当然,你的应用程序可能有适合左侧风格的艺术风格要求(想想《我的世界》),但右侧在传统图形应用程序中是首选。 采样器对象在从纹理读取颜色时会自动为你应用这种过滤。
欠采样是相反的问题,即纹素数多于片段数。 当以尖锐角度采样高频图案(如棋盘纹理)时,这将导致伪影:
如左侧图像所示,纹理在远处变成模糊的一团。 解决方案是 各向异性过滤,它也可以由采样器自动应用。
除了这些过滤器,采样器还可以处理变换。 它通过其 寻址模式 决定当你尝试读取图像外部纹素时会发生什么。 下图显示了一些可能性:
我们现在将创建一个函数 createTextureSampler 来设置这样一个采样器对象。
稍后我们将使用该采样器从着色器中的纹理读取颜色。
void initVulkan()
{
...
createTextureImage();
createTextureImageView();
createTextureSampler();
...
}
...
void createTextureSampler()
{
}
采样器通过 vk::SamplerCreateInfo 结构体进行配置,该结构体指定了它应应用的所有过滤器和变换。
vk::SamplerCreateInfo samplerInfo{.magFilter = vk::Filter::eLinear, .minFilter = vk::Filter::eLinear};
magFilter 和 minFilter 字段指定如何插值被放大或缩小的纹素。
放大涉及上述过采样问题,缩小涉及欠采样。
选择是 vk::Filter::eNearest 和 vk::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 结构的文档,你会看到它包含一个名为 limits 的 vk::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 不同,它们将纹理图像和过滤合并为一个状态。
如果你现在运行程序,你会看到类似这样的验证层消息:
这是因为各向异性过滤实际上是一个可选的设备功能。
我们需要更新 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;
在下一章中,我们将把图像和采样器对象暴露给着色器,以将纹理绘制到正方形上。
C++ 代码 / slang 着色器 / GLSL 顶点着色器 / GLSL 片段着色器
Vulkan 是 Khronos Group Inc. 的注册商标
教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。