我们之前也有处理过具体的贴图LODVulkan填坑学习Day27-1—贴图LOD(mipmap),本部分我们重新来审视一下:如何使用图像的位块和适当的图像屏障在运行时根据基本图像生成完整的纹理mipmap以及纹理LOD的简单使用,并且我们创建不同的采样器来查看下线性或各向异性采样的结果。
接下来我们主要来尝试如何在运行时生成完整的纹理Mip链,而不是从纹理文件加载脱机生成的mip贴图。虽然这种方式通常不适用于存储在磁盘上的纹理(通常将mips脱机生成并存储在文件中,并在加载的时候分级加载mipmap),但是该技术用于在运行时生成纹理,例如,在执行动态立方体贴图或其他渲染时-纹理效果。
对于运行时生成的纹理,具有mip贴图在图像稳定性和性能方面都具有很多好处。如果没有mip映射,则图像将变得嘈杂,尤其是在高频纹理(以及诸如镜面反射的纹理分量)的情况下,并且由于缓存,使用mip映射将导致更高的性能。
我们本次尝试的是在开始时仅为单个纹理生成一个Mip链,但该技术也可以在正常帧渲染期间使用,以为动态纹理生成Mip链。
一、vulkan理论
在创建mipmap之前我们需要来了解以下几个VKAPI常用概念:
1.1 图像拉伸
3D渲染的一个常见瓶颈就是片段着色器的计算代价过大。为了减小片段着色器的计算压力,我们可以将游戏场景以一个较小的分辨率进行渲染,然后放大到设备原来的屏幕大小进行显示。
- render pass A:使用较小的分辨率
- 渲染游戏场景
- render pass B:使用设备的原始分辨率
- 放大游戏场景图像
- 渲染UI
Vulkan支持格式转换和改变复制区域的尺寸可以通过调用vkCmdBlitImage函数
- a.复制源图像到目标图像,可能进行潜在的格式转换,缩放和过滤操作
- b.这一操作可能使用GPU或CPU的专有硬件
tips: 通过绘制一个占满全屏幕的四边形看上去,调用vkCmdBlitImage函数更好。
vkCmdBlitImage()可以接受不同格式的图像并展开或缩小需要复制的区域,当这些区域写入到目标图像时。术语blit是 block image transfer的缩写,指不仅需要复制图像数据,并且也可能需要在此过程中处理数据。vkCmdBlitImage()的原型如下:
VKAPI_ATTR void VKAPI_CALL vkCmdBlitImage(
VkCommandBuffer commandBuffer,
VkImage srcImage,
VkImageLayout srcImageLayout,
VkImage dstImage,
VkImageLayout dstImageLayout,
uint32_t regionCount,
const VkImageBlit* pRegions,
VkFilter filter);
将执行此命令的命令缓冲区通过commandBuffer参数传递。源图像和目标图像各通过srcImage 和dstImage 参数传递。再有,和vkCmdCopyImage()一样,期望的源图像和目标图像的布局通过srcImageLayout 和dstImageLayout参数传递。源图像的布局是VK_IMAGE_LAYOUT_GENERAL 或者VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,目标图像的布局是VK_IMAGE_LAYOUT_GENERAL 或者VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMA。
和其他的复制命令一样,vkCmdBlitImage()可复制源图像的任意数量区域到目标图像,每一个区域都通过一个数据结构表示。区域的数量由regionCount参数传递,pRegion指向了一个大小为regionCount的数组,每一个元素定义了一个需要复制的区域。VkImageBlit的定义为:
typedef struct VkImageBlit {
VkImageSubresourceLayers srcSubresource;
VkOffset3D srcOffsets[2];
VkImageSubresourceLayers dstSubresource;
VkOffset3D dstOffsets[2];
} VkImageBlit;
VkImageBlit的srcSubresource 和dstSubresource域定义了源图像和目标图像的子资源。然而,在VkImageCopy中每一个区域都被一个VkOffset3D类型的数据定义,并共享一个VkExtent3D,在VkImageBlit中每一个区域都通过一对VkOffset3D数据(两个元素的数组)定义。
srcOffsets 和dstOffsets数组的第一个元素定义了将被复制的区域的一侧,数组的第二个元素定义了区域的另外一侧。源图像中srcOffsets定义的区域被复制到了dstOffsets定义的目标图像。如果这两个区域其中一个对于另外一个是“上下颠倒的”,那么复制的区域将会被垂直翻转。同理,如果一个区域相对域另外一个“前后颠倒的”,那么图像就会被水平翻转。如果这两种情况都满足,那么复制的区域和原区域相比就被旋转180度了。
如果在源区域和目标区域的巨型尺寸不相同,那么图像就会被相应的放大或缩小。这种情况下,vkCmdBlitImage()命令filter参数定义的过滤模式将会被应用到数据的过滤上。Filter必须是VK_FILTER_NEAREST 或VK_FILTER_LINEAR其一,分别用于点采样或线性采样。
源图像的格式必须是支持VK_FORMAT_FEATURE_BLIT_SRC_BIT特性的其中一种。在绝大多数Vulkan实现中,这几乎包含了所有的图像格式。还有,目标图像格式必须是支持VK_FORMAT_FEATURE_BLIT_DST_BIT的其中一种。通常,这是任何可以在shader内渲染到或者设备可写入的格式。Vulkan设备不大有可能会支持在压缩图像格式上blit操作。
1.2 屏障Barrier
Barrier是同一个queue中的command,或者同一个subpass中的command所明确指定的依赖关系。barrier的中文释义一般叫栅栏或者屏障,我们可以想象一下有一大串的command乱序执行(实际上可能是顺序开始,乱序结束),barrier就是在中间树立一道栅栏,要求栅栏前后保持一定的顺序。
vkCmdPipelineBarrier API可以用于创建一个Pipeline中的Barrier。注意这个API与fence/semaphore的不同,这个API的前缀是vkCmd,这意味着这是一个向command buffer中记录命令的API:
void vkCmdPipelineBarrier(
VkCommandBuffer commandBuffer,
VkPipelineStageFlags srcStageMask,
VkPipelineStageFlags dstStageMask,
VkDependencyFlags dependencyFlags,
uint32_t memoryBarrierCount,
const VkMemoryBarrier* pMemoryBarriers,
uint32_t bufferMemoryBarrierCount,
const VkBufferMemoryBarrier* pBufferMemoryBarriers,
uint32_t imageMemoryBarrierCount,
const VkImageMemoryBarrier* pImageMemoryBarriers);
第一个参数command buffer就是这个命令将要被记录的command buffer。pipe line barrier涉及到两个同步范围,这两个同步范围所处的stage分别由srcStageMask和dstStageMask指明,注意着两个都是mask,所以每一个都可以设置多个阶段。dependencyFlags是个比较高级的使用方法。
然后就是由vulkan API一贯味道的三个数组了 ,分别是memoryBarrier、bufferMemoryBarrier以及最后的ImageMemoryBarrier。当着三个数组都为空的时候,将会在当前执行环境创建一个Execution Barrier,否则,则创建一个Memory Barrier。
屏障使用示例
Execution Barrier就是简单的执行屏障。我们这里举一个来自于khronos官方的例子。
vkCmdDispatch(...);
vkCmdPipelineBarrier(
...
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, // srcStageMask
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, // dstStageMask
...);
vkCmdDispatch(...);
两个dispatch中,如果涉及到执行的先后顺序,就需要一个execution barrier。如果没有这个barrier,那么这两个dispatch先后顺序是无法预测的。很可能的情况是,第一个dispatch开始后,第二个dispatch也马上开始。至于谁先结束,无法预测。
Execution Barrier可以有效控制queue中的command或者subpass中的command的执行顺序。
这个示例使用了vulkan中的compute path,毕竟这条路比graphic那条路简单了许多。对于compute path而言,可能的stage只有top, indirect, compute/transfer, bottom。如果是graphic path,可能的stage就多了去了。后续有机会我们会再介绍这个。现在只有理解个大概就可以了。
tips: Execution Barrier只保证了执行的顺序,对于存储修改的顺序,execution barrier无能为力。为了解决execution barrier无法控制存储的缺点,vulkan需要引入新的barrier,即memory barrier。有兴趣的可自行查找相关资料查看。
我们本部分主要是使用图像内存屏障来控制对图形的访问。
二、vulkan实现
全局变量:
//基础纹理数据
struct Texture {
VkImage image;
VkDeviceMemory deviceMemory;
VkImageView view;
uint32_t width, height;
uint32_t mipLevels;
} texture;
// 纹理采样器,用于显示不同效果(无mipmap采样器、双线性纹理采样、各向异性纹理采样)
std::vector<VkSampler> samplers;
2.1 mipmap生成
首先说一下大致的执行过程:
- 加载第一级的纹理图片并计算mip等级
- 生成Mip链
- 最终图像布局过渡
- 图像视图创建
首先创建一个函数来执行对应的管线屏障
void insertImageMemoryBarrier(
VkCommandBuffer cmdbuffer,
VkImage image,
VkAccessFlags srcAccessMask,
VkAccessFlags dstAccessMask,
VkImageLayout oldImageLayout,
VkImageLayout newImageLayout,
VkPipelineStageFlags srcStageMask,
VkPipelineStageFlags dstStageMask,
VkImageSubresourceRange subresourceRange)
{
VkImageMemoryBarrier imageMemoryBarrier = vks::initializers::imageMemoryBarrier();
imageMemoryBarrier.srcAccessMask = srcAccessMask;
imageMemoryBarrier.dstAccessMask = dstAccessMask;
imageMemoryBarrier.oldLayout = oldImageLayout;
imageMemoryBarrier.newLayout = newImageLayout;
imageMemoryBarrier.image = image;
imageMemoryBarrier.subresourceRange = subresourceRange;
vkCmdPipelineBarrier(cmdbuffer,srcStageMask,dstStageMask,0,0, nullptr,0, nullptr,1, &imageMemoryBarrier);
}
首先我们创建一个单独的纹理加载loadTexture()函数来单独处理mipmap,具体步骤见代码注释:
2.1.1 LOD等级计算
即使最初只上传第一个Mip级别,我们也会创建具有所需Mip级别数量的图像。以下公式用于基于最大 图片范围:
texture.mipLevels = floor(log2(std :: max(texture.width,texture.height)))+ 1 ;
然后将其传递给图像创建信息:
VkImageCreateInfo imageCreateInfo = vkTools::initializers::imageCreateInfo();
imageCreateInfo.imageType = VK_IMAGE_TYPE_2D;
imageCreateInfo.format = format;
imageCreateInfo.mipLevels = texture.mipLevels;
...
必须设置所需的Mip级别数,因为它用于为图像(vkAllocateMemory)分配合适的内存量。
2.1.2 更新各级mipmap
在生成Mip链之前,我们需要将从磁盘加载的图像数据复制到新生成的图像中。该图像将成为我们的Mip链的基础:
VkBufferImageCopy bufferCopyRegion = {};
bufferCopyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
bufferCopyRegion.imageSubresource.mipLevel = 0;
bufferCopyRegion.imageExtent.width = texture.width;
bufferCopyRegion.imageExtent.height = texture.height;
bufferCopyRegion.imageExtent.depth = 1;
vkCmdCopyBufferToImage(copyCmd, stagingBuffer, texture.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &bufferCopyRegion);
2.1.3 准备基本mip数据
当我们要从基础mip级别中获取数据时,我们还需要设置插入图像内存屏障,将基本mip级别的图像布局设置为TRANSFER_SRC:
VkImageSubresourceRange subresourceRange = {};
subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
subresourceRange.levelCount = 1;
subresourceRange.layerCount = 1;
insertImageMemoryBarrier(
copyCmd,
texture.image,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_TRANSFER_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
subresourceRange);
2.1.4 生成mip链
生成mip链有两种不同的方法。第一种是将整个mip链从n-1级降低到n级,另一种方式是始终使用基本图像,然后从基础级降低到所有级别。本示例使用第一个示例。
我们简单地遍历所有剩余的Mip级别(从磁盘加载了0级),并VkImageBlit为从Mip级i-1到i级的每个blit准备了一个结构。
首先是blit的来源。这是先前的第一级mip。blit源的尺寸由srcOffset指定:
for (int32_t i = 1; i < texture.mipLevels; i++)
{
VkImageBlit imageBlit{};
// Source
imageBlit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
imageBlit.srcSubresource.layerCount = 1;
imageBlit.srcSubresource.mipLevel = i-1;
imageBlit.srcOffsets[1].x = int32_t(texture.width >> (i - 1));
imageBlit.srcOffsets[1].y = int32_t(texture.height >> (i - 1));
imageBlit.srcOffsets[1].z = 1;
设置目标Mip级别(1),并在dstOffsets [1]中指定blit目标的尺寸:
// 源
imageBlit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
imageBlit.dstSubresource.layerCount = 1;
imageBlit.dstSubresource.mipLevel = i;
imageBlit.dstOffsets[1].x = int32_t(texture.width >> i);
imageBlit.dstOffsets[1].y = int32_t(texture.height >> i);
imageBlit.dstOffsets[1].z = 1;
在我们达到此Mip级别之前,我们需要将其图像布局转换为TRANSFER_DST:
VkImageSubresourceRange mipSubRange = {};
mipSubRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
mipSubRange.baseMipLevel = i;
mipSubRange.levelCount = 1;
mipSubRange.layerCount = 1;
// 准备当前的mip级别作为映像blit的目标
insertImageMemoryBarrier(
blitCmd,
texture.image,
0,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
mipSubRange);
注意,我们设置了baseMipLevel子资源范围的成员,因此图像内存屏障将仅影响我们要复制到的一个Mip级别。
现在,我们要复制的Mip级别和要复制的Mip级别都处于正确的布局中(传输源和目标),我们可以发出vkCmdBlitImage要从Mip级别(i-1)复制到Mip级别(i)的布局:
vkCmdBlitImage(
blitCmd,
texture.image,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
texture.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1,
&imageBlit,
VK_FILTER_LINEAR);
vkCmdBlitImage 使用线性滤波器将(降低)从Mip级别(i-1)缩放到Mip级别(i)。
绘制完成后,我们可以将此Mip级别用作下一级别的基础,因此可以将布局从转换为TRANSFER_DST_OPTIMAL,TRANSFER_SRC_OPTIMAL以便可以将该级别用作下一级别的传输源:
// 准备当前的mip级别作为下一个级别的图像blit源
insertImageMemoryBarrier(
copyCmd,
texture.image,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_TRANSFER_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
mipSubRange);
}
2.1.5 最终图像布局过渡
循环完成后,我们需要将图像的所有Mip级别转换为它们的实际使用布局。并且在循环之后,所有级别都将位于TRANSER_SRC布局中,从而使我们可以立即传输整个图像:
subresourceRange.levelCount = texture.mipLevels;
insertImageMemoryBarrier(
copyCmd,
texture.image,
VK_ACCESS_TRANSFER_READ_BIT,
VK_ACCESS_SHADER_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
subresourceRange);
提交该命令缓冲区将导致图像具有完整的Mip链,并且所有MIP级别都将转换为用于着色器读取的正确图像布局。
2.1.6 图像视图创建
图像视图还需要有关使用了多少个Mip级别的信息。在VkImageViewCreateInfo.subresourceRange.levelCount字段中指定。
VkImageViewCreateInfo view = vks::initializers::imageViewCreateInfo();
view.image = texture.image;
view.viewType = VK_IMAGE_VIEW_TYPE_2D;
view.format = format;
view.components = { VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A };
view.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
view.subresourceRange.baseMipLevel = 0;
view.subresourceRange.baseArrayLayer = 0;
view.subresourceRange.layerCount = 1;
view.subresourceRange.levelCount = texture.mipLevels;
全部代码如下:
void loadTexture(std::string filename, VkFormat format, bool forceLinearTiling)
{
ktxResult result;//纹理加载是否成功
ktxTexture* ktxTexture;//纹理数据
if (!vks::tools::fileExists(filename)) {
vks::tools::exitFatal("Could not load texture from " , -1);
}
result = ktxTexture_CreateFromNamedFile(filename.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktxTexture);
assert(result == KTX_SUCCESS);
texture.width = ktxTexture->baseWidth;
texture.height = ktxTexture->baseHeight;
ktx_uint8_t *ktxTextureData = ktxTexture_GetData(ktxTexture);
ktx_size_t ktxTextureSize = ktxTexture_GetImageSize(ktxTexture, 0);
// 计算mipmap的个数
texture.mipLevels = floor(log2(std::max(texture.width, texture.height))) + 1;
//获取请求的纹理格式的设备属性
VkFormatProperties formatProperties;
vkGetPhysicalDeviceFormatProperties(physicalDevice, format, &formatProperties);
// Mip链的生成需要图像位块源和目标的支持
assert(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_SRC_BIT);
assert(formatProperties.optimalTilingFeatures & VK_FORMAT_FEATURE_BLIT_DST_BIT);
VkMemoryAllocateInfo memAllocInfo = vks::initializers::memoryAllocateInfo();
VkMemoryRequirements memReqs = {};
//创建一个包含原始图像数据的主机可见的暂存缓冲区
VkBuffer stagingBuffer;
VkDeviceMemory stagingMemory;
VkBufferCreateInfo bufferCreateInfo = vks::initializers::bufferCreateInfo();
bufferCreateInfo.size = ktxTextureSize;
//此缓冲区用作缓冲区副本的传输源
bufferCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
bufferCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
VK_CHECK_RESULT(vkCreateBuffer(device, &bufferCreateInfo, nullptr, &stagingBuffer));
vkGetBufferMemoryRequirements(device, stagingBuffer, &memReqs);
memAllocInfo.allocationSize = memReqs.size;
memAllocInfo.memoryTypeIndex = vulkanDevice->getMemoryType(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT);
VK_CHECK_RESULT(vkAllocateMemory(device, &memAllocInfo, nullptr, &stagingMemory));
VK_CHECK_RESULT(vkBindBufferMemory(device, stagingBuffer, stagingMemory, 0));
//复制纹理数据到暂存缓冲区
uint8_t *data;
VK_CHECK_RESULT(vkMapMemory(device, stagingMemory, 0, memReqs.size, 0, (void **)&data));
memcpy(data, ktxTextureData, ktxTextureSize);
vkUnmapMemory(device, stagingMemory);
//创建最佳平铺目标图像
VkImageCreateInfo imageCreateInfo = vks::initializers::imageCreateInfo();
imageCreateInfo.imageType = VK_IMAGE_TYPE_2D;
imageCreateInfo.format = format;
imageCreateInfo.mipLevels = texture.mipLevels;
imageCreateInfo.arrayLayers = 1;
imageCreateInfo.samples = VK_SAMPLE_COUNT_1_BIT;
imageCreateInfo.tiling = VK_IMAGE_TILING_OPTIMAL;
imageCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
imageCreateInfo.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
imageCreateInfo.extent = { texture.width, texture.height, 1 };
imageCreateInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
VK_CHECK_RESULT(vkCreateImage(device, &imageCreateInfo, nullptr, &texture.image));
vkGetImageMemoryRequirements(device, texture.image, &memReqs);
memAllocInfo.allocationSize = memReqs.size;
memAllocInfo.memoryTypeIndex = vulkanDevice->getMemoryType(memReqs.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);
VK_CHECK_RESULT(vkAllocateMemory(device, &memAllocInfo, nullptr, &texture.deviceMemory));
VK_CHECK_RESULT(vkBindImageMemory(device, texture.image, texture.deviceMemory, 0));
VkCommandBuffer copyCmd = vulkanDevice->createCommandBuffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);
VkImageSubresourceRange subresourceRange = {};
subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
subresourceRange.levelCount = 1;
subresourceRange.layerCount = 1;
// 最佳图像将被用作复制的目的地,因此我们必须从初始未定义的图像布局传输到传输目的地布局
insertImageMemoryBarrier(
copyCmd,
texture.image,
0,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
subresourceRange);
//复制链中的第一个mip,剩余的mip将程序生成
VkBufferImageCopy bufferCopyRegion = {};
bufferCopyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
bufferCopyRegion.imageSubresource.mipLevel = 0;
bufferCopyRegion.imageSubresource.baseArrayLayer = 0;
bufferCopyRegion.imageSubresource.layerCount = 1;
bufferCopyRegion.imageExtent.width = texture.width;
bufferCopyRegion.imageExtent.height = texture.height;
bufferCopyRegion.imageExtent.depth = 1;
vkCmdCopyBufferToImage(copyCmd, stagingBuffer, texture.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &bufferCopyRegion);
// 在位块传送期间将第一个mip级别转换为读取源
insertImageMemoryBarrier(
copyCmd,
texture.image,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_TRANSFER_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
subresourceRange);
vulkanDevice->flushCommandBuffer(copyCmd, queue, true);
//清理暂存缓冲区资源
vkFreeMemory(device, stagingMemory, nullptr);
vkDestroyBuffer(device, stagingBuffer, nullptr);
ktxTexture_Destroy(ktxTexture);
//生成mip链
// ---------------------------------------------------------------
//我们通过从mip-1到mip的位块复制整个mip链
VkCommandBuffer blitCmd = vulkanDevice->createCommandBuffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);
//从n - 1复制mips到n
for (int32_t i = 1; i < texture.mipLevels; i++)
{
VkImageBlit imageBlit{};
// 源
imageBlit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
imageBlit.srcSubresource.layerCount = 1;
imageBlit.srcSubresource.mipLevel = i-1;
imageBlit.srcOffsets[1].x = int32_t(texture.width >> (i - 1));
imageBlit.srcOffsets[1].y = int32_t(texture.height >> (i - 1));
imageBlit.srcOffsets[1].z = 1;
// 目标
imageBlit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
imageBlit.dstSubresource.layerCount = 1;
imageBlit.dstSubresource.mipLevel = i;
imageBlit.dstOffsets[1].x = int32_t(texture.width >> i);
imageBlit.dstOffsets[1].y = int32_t(texture.height >> i);
imageBlit.dstOffsets[1].z = 1;
VkImageSubresourceRange mipSubRange = {};
mipSubRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
mipSubRange.baseMipLevel = i;
mipSubRange.levelCount = 1;
mipSubRange.layerCount = 1;
//将当前mip级别准备为图像blit目标
insertImageMemoryBarrier(
blitCmd,
texture.image,
0,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_IMAGE_LAYOUT_UNDEFINED,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
mipSubRange);
//上一级的Blit
vkCmdBlitImage(
blitCmd,
texture.image,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
texture.image,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
1,
&imageBlit,
VK_FILTER_LINEAR);
//准备当前的mip级别作为下一级的图像blit源
insertImageMemoryBarrier(
blitCmd,
texture.image,
VK_ACCESS_TRANSFER_WRITE_BIT,
VK_ACCESS_TRANSFER_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_TRANSFER_BIT,
mipSubRange);
}
//在循环之后,所有的mip层都在TRANSFER_SRC布局中,所以转换为着色器可读
subresourceRange.levelCount = texture.mipLevels;
insertImageMemoryBarrier(
blitCmd,
texture.image,
VK_ACCESS_TRANSFER_READ_BIT,
VK_ACCESS_SHADER_READ_BIT,
VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
VK_PIPELINE_STAGE_TRANSFER_BIT,
VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
subresourceRange);
vulkanDevice->flushCommandBuffer(blitCmd, queue, true);
// ---------------------------------------------------------------
// 创建取样器
samplers.resize(3);
VkSamplerCreateInfo sampler = vks::initializers::samplerCreateInfo();
sampler.magFilter = VK_FILTER_LINEAR;
sampler.minFilter = VK_FILTER_LINEAR;
sampler.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
sampler.addressModeU = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
sampler.addressModeV = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
sampler.addressModeW = VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT;
sampler.mipLodBias = 0.0f;
sampler.compareOp = VK_COMPARE_OP_NEVER;
sampler.minLod = 0.0f;
sampler.maxLod = 0.0f;
sampler.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
sampler.maxAnisotropy = 1.0;
sampler.anisotropyEnable = VK_FALSE;
// 没有mip映射
VK_CHECK_RESULT(vkCreateSampler(device, &sampler, nullptr, &samplers[0]));
// 有mip映射
sampler.maxLod = (float)texture.mipLevels;
VK_CHECK_RESULT(vkCreateSampler(device, &sampler, nullptr, &samplers[1]));
// 具有mip映射和各向异性滤波
if (vulkanDevice->features.samplerAnisotropy)
{
sampler.maxAnisotropy = vulkanDevice->properties.limits.maxSamplerAnisotropy;
sampler.anisotropyEnable = VK_TRUE;
}
VK_CHECK_RESULT(vkCreateSampler(device, &sampler, nullptr, &samplers[2]));
// 创建图像视图
VkImageViewCreateInfo view = vks::initializers::imageViewCreateInfo();
view.image = texture.image;
view.viewType = VK_IMAGE_VIEW_TYPE_2D;
view.format = format;
view.components = { VK_COMPONENT_SWIZZLE_R, VK_COMPONENT_SWIZZLE_G, VK_COMPONENT_SWIZZLE_B, VK_COMPONENT_SWIZZLE_A };
view.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
view.subresourceRange.baseMipLevel = 0;
view.subresourceRange.baseArrayLayer = 0;
view.subresourceRange.layerCount = 1;
view.subresourceRange.levelCount = texture.mipLevels;
VK_CHECK_RESULT(vkCreateImageView(device, &view, nullptr, &texture.view));
}
2.2 描述符资源
简单回顾下描述符资源的使用:
- 描述符资源管线布局
- 创建描述符池
- 创建描述符集及资源绑定
具体代码便不再赘述
2.3 着色器采样
最后我们来看一下着色器采样,中规中矩没有过多高级操作:
顶点着色器:
#version 450
layout (location = 0) in vec3 inPos;
layout (location = 1) in vec2 inUV;
layout (location = 2) in vec3 inNormal;
layout (binding = 0) uniform UBO
{
mat4 projection;
mat4 view;
mat4 model;
vec4 viewPos;
float lodBias;//LOD级别
int samplerIndex;//无mipmap采样、双线性纹理采样、各向异性纹理采样
} ubo;
layout (location = 0) out vec2 outUV;
layout (location = 1) out float outLodBias;
layout (location = 2) flat out int outSamplerIndex;
layout (location = 3) out vec3 outNormal;
layout (location = 4) out vec3 outViewVec;
layout (location = 5) out vec3 outLightVec;
out gl_PerVertex
{
vec4 gl_Position;
};
void main()
{
outUV = inUV * vec2(2.0, 1.0);
outLodBias = ubo.lodBias;
outSamplerIndex = ubo.samplerIndex;
vec3 worldPos = vec3(ubo.model * vec4(inPos, 1.0));
gl_Position = ubo.projection * ubo.view * ubo.model * vec4(inPos.xyz, 1.0);
outNormal = mat3(inverse(transpose(ubo.model))) * inNormal;
vec3 lightPos = vec3(-30.0, 0.0, 0.0);
outLightVec = worldPos - lightPos;
outViewVec = ubo.viewPos.xyz - worldPos;
}
片元着色器:
#version 450
layout (set = 0, binding = 1) uniform texture2D textureColor;
layout (set = 0, binding = 2) uniform sampler samplers[3];
layout (location = 0) in vec2 inUV;
layout (location = 1) in float inLodBias;
layout (location = 2) flat in int inSamplerIndex;
layout (location = 3) in vec3 inNormal;
layout (location = 4) in vec3 inViewVec;
layout (location = 5) in vec3 inLightVec;
layout (location = 0) out vec4 outFragColor;
void main()
{
vec4 color = texture(sampler2D(textureColor, samplers[inSamplerIndex]), inUV, inLodBias);
vec3 N = normalize(inNormal);
vec3 L = normalize(inLightVec);
vec3 V = normalize(inViewVec);
vec3 R = reflect(L, N);
vec3 diffuse = max(dot(N, L), 0.65) * vec3(1.0);
float specular = pow(max(dot(R, V), 0.0), 16.0) * color.a;
outFragColor = vec4(diffuse * color.rgb + specular, 1.0);
}
LOD效果可见开头动图,各种采样运行可见如下效果对比: