Loading models
我们将使用tinyobjloader库从OBJ文件加载顶点和索引,它速度快,易于集成,因为它是一个像stb_image一样的单一文件库
因为我们没有学习光照,使用照明烘焙的纹理
在程序中添加两个新的配置变量来定义模型和纹理路径:
并更新createTextureImage 以使用此路径变量加载纹理数据
Loading vertices and indices
模型加载和简单的图形没什么太大区别,主要就是vertices and indices的变更
我们现在要从模型文件中加载顶点和索引,所以你现在应该删除全局顶点和索引数组。将它们替换为非常量容器作为类成员
因为这次的索引很多,因此将索引的类型从uint16_t改为uint32_t,也不要忘记更改vkCmdBindIndexBuffer中的类型
LoadObj
包含tiny_obj_loader.h文件
添加新的loadModel函数,通过
if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, MODEL_PATH.c_str()))
加载模型路径,获取所有的顶点和索引数据,放到刚刚的vertices and indices数据结构中
OBJ文件由位置、法线、纹理坐标和面组成
- attrib容器包括attrib.vertices、attrib.normals和attrib.texcoords的属性
- shapes包含所有单独的面(每个面有三个顶点),每个面由一个顶点数组组成,每个顶点包含位置、法线和纹理坐标属性的索引,
- materials材质
- err字符串包含错误
- warn字符串包含加载文件时发生的警告
我们要循环所有的shapes对象,以便循环所有的对象网格索引shape.mesh.indices,以便查找attrib数组中的实际顶点属性:
shape.mesh.indices其中包含vertex_index、normal_index和texcoord_index成员的索引(首个顶点索引,3个顶点依次递增),还需要将索引乘以3(因为存储顺序可能是x0, x1, x2, y0, y1, y2, z0, z1, z2…)
但是还有一个问题,OBJ格式假设一个坐标系,其中垂直坐标为0表示图像的底部,但是到Vulkan中,其中0表示图像的顶部,因此通过翻转纹理坐标的垂直分量来解决此问题
Mipmaps
多级贴图 / 多级渐远纹理,每个新图像的宽度和高度都是前一个图像的一半,Mipmap用作细节级别或LOD(细节级别)的一种形式。远离相机的物体将从较小的mip图像中采样其纹理。使用较小的图像可提高渲染速度并避免摩尔纹等伪影
miplevels
首先指定miplevels的级别数量,这个值是根据图像的尺寸计算,在stbi_load获得了(texWidth, texHeight)之后计算
mipLevels = static_cast<uint32_t>(std::floor(std::log2(std::max(texWidth, texHeight)))) + 1;
max函数选择最大的维度。log2函数计算该维度可以被2整除的次数。floor函数处理最大维度不是2的幂的情况。1,使得原始图像具有mip级别
我们需要更改mip Image、mip ImageView和transitionImageLayout函数,将miplevels作为参数,并更改内部info的levelCount成员,以及所有调用这些函数的地方(对textureImage使用mipLevels ,对depthimage使用1)
这样image缓冲中就可以存储所有mip级别图像
Generating Mipmaps
我们的暂存stagingBuffer只能通过vkCmdCopyBufferToImage填充miplevels== 0 的纹理层,对于其他级别需要调用vkCmdBlitImage(位块传输)命令执行复制、缩放和筛选操作,以便将数据传输到纹理图像的每一层
我们现在打算从miplevels==0级别的纹理传输到其他级别图像中,因此要指定VK_IMAGE_USAGE_TRANSFER_SRC_BIT 标志
我们依旧要考虑图像转换时的布局,但这次不再是一个图像转换,要考虑miplevels个图像转换,因此我们需要编写更多的vkCmdPipelineBarrier命令转换每个图像以便被着色器使用,
删除现有到 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL的transitionImageLayout过渡,编写新的生成mipmaps的函数
对于VkImageMemoryBarrier结构体的部分成员,每次转换都保持不变,因此不需要放在for中,subresourceRange.miplevel、oldLayout、newLayout、srcallbackMask和dstallback Mask将针对每个转换进行更改(i从1开始)
实例
我们举例miplevels == 0时:
首先通过vkCmdPipelineBarrier命令将布局转换为VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,它可以等待从上一个vkCmdBlitImage命令或从vkCmdCopyBufferToImage填充(转换为源可以填充DST)
接下来创建VkImageBlit
- 源mip级别是i - 1,目的地mip级别是i
- srcOffsets数组的两个元素决定了数据将从哪个3D区域进行位块传输,{ mipWidth, mipHeight, 1 };
- dstOffsets确定数据将被传输到的区域,dstOffsets[1]的X和Y维度被除以2,因为每个mip级别是前一级别大小的一半 { mipWidth > 1 ? mipWidth / 2 : 1, mipHeight > 1 ? mipHeight / 2 : 1, 1 };
- 其中Z维度必须为1,因为2D图像的深度为1
使用vkCmdBlitImage命令传输数据,srcImage和dstImage的布局分别为SRC和DST(刚才转换的布局),最后一个参数时filter过滤选项
再次使用vkCmdPipelineBarrier命令将布局转换为VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,以便供着色器使用
在每次循环结束,我们将当前的mip维度除以2,除法之前检查每个维度,以确保维度永远不会小于1
在for结束之前,我们再插入一个vkCmdPipelineBarrier转换为 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL 。这不是由循环处理的,因为最后一个mip级别永远不会被blitted。
Linear filtering support
使用vkCmdBlitImage内置函数来生成所有的mip级别不能保证在所有平台上都支持它,需要图像格式支持线性过滤
vkGetPhysicalDeviceFormatProperties获取图像格式(包括3种linearTilingFeatures、optimalTilingFeatures和bufferFeatures),对线性过滤功能的支持可以使用 optimalTilingFeatures & VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT 检查
Sampler
当VkImage缓冲保存mipmap数据时,VkSampler控制渲染时如何读取该数据允许我们指定,修改VkSamplerCreateInfo结构体成员
如果mipmapMode为 VK_SAMPLER_MIPMAP_MODE_NEAREST,则采样一个mip级别,如果mipmap模式为VK_SAMPLER_MIPMAP_MODE_LINEAR,则采样的两个mip级别,并将结果线性混合
如果对象靠近相机,则使用magFilter作为滤镜。如果对象距离相机较远,则使用minFilter
mipLodBias允许我们强制Vulkan使用比正常情况下更低的lod和level
samplerInfo.minLod = static_cast< float >(mipLevels / 2);