【Vulkan学习记录-基础篇-3】纹理和模型导入、生成Mipmap

前两篇的代码是在太过于冗杂,于是我做了一下简单的封装,现在的代码结构为:
【Vulkan学习记录-基础篇-3】纹理和模型导入、生成Mipmap_第1张图片
VKApp 负责初始化窗口以及VkInstance、VkDevice
VKResourceCreator 负责创建各种资源(Image、ImageView、Buffer等)
VKScene 负责初始化Pipeline RenderPass等,在每一帧中执行渲染
VKCommand 负责创建所有可复用的命令(CopyBuffer等)

源码已经传到git上了:proj

导入纹理和模型会涉及到纹理图片的加载以及模型文件的加载,这两个都使用第三方库来完成,
stb:纹理加载库stb
tinyobjloader:模型加载库tinyobjloader
这两个库都非常地轻量,只需要把它们的头文件添加到工程中就可以了。

部署源码时请手动添加Vulkan、GLFW的头文件和静态链接库,GLM以及以上两个库的头文件。(CMakeList还没有完备地去写)

本节最终的效果为:
【Vulkan学习记录-基础篇-3】纹理和模型导入、生成Mipmap_第2张图片

纹理的加载

加载纹理需要以下几步:
1.从纹理文件中读取数据到内存
2.创建一个staging buffer和一个VkImage
3.将读取后的数据转移到staging buffer中
4.对VkImage做Layout转移,将它转移到VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL中
以供数据拷贝
5.将staging buffer中的数据拷贝的VkImage中
6.对VkImage做Layout转移,将它转移到VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL供Shader读取使用(如果要生成Mipmap,则需要把这一步去掉,原因在后面会讲)

void VKResourceCreator::CreateTexture(const char * file , VkImage & textureImage , VkDeviceMemory & textureImageMemory , VKCommandManager * commandManager , VkQueue queue , VkCommandPool commandPool, int& width, int& height , uint32_t &mipLevels)
{
	// 1. load data from file 
	int texWidth, texHeight, texChannels;
	stbi_uc* pixels = stbi_load(file, &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);
	VkDeviceSize imageSize = texWidth * texHeight * 4;
	mipLevels = std::floor(std::log2(std::max(texWidth, texHeight))) + 1;
	width = texWidth;
	height = texHeight;
	if (!pixels)
	{
		throw std::runtime_error("failed to load texture image . ");
	}

	// 2. create staging buffer and image 
	VkBuffer stagingBuffer;
	VkDeviceMemory stagingBufferMemory;
	CreateBuffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, stagingBuffer, stagingBufferMemory);

	CreateImage(texWidth,
		texHeight,
		mipLevels,
		VK_FORMAT_R8G8B8A8_UNORM,
		VK_IMAGE_TILING_OPTIMAL,
		VK_SAMPLE_COUNT_1_BIT,
		VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT,
		VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT,
		textureImage,
		textureImageMemory);

	// 3. transition data into staging buffer
	void* data;
	vkMapMemory(logical_device_, stagingBufferMemory, 0, imageSize, 0, &data);
	memcpy(data, pixels, static_cast(imageSize));
	vkUnmapMemory(logical_device_, stagingBufferMemory);
	stbi_image_free(pixels);

	// 4. transition layout 1
	commandManager->TransitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM, mipLevels, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, queue, commandPool);

	// 5. copy data into image
	commandManager->CopyBufferToImage(stagingBuffer, textureImage, texWidth, texHeight, queue, commandPool );

	// 6. transition layout 2
	commandManager->TransitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM, mipLevels, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, queue, commandPool );

	vkDestroyBuffer(logical_device_, stagingBuffer, NULL);
	vkFreeMemory(logical_device_, stagingBufferMemory, NULL);
}

注意在创建Texture的时候给Usage添加了一个VK_IMAGE_USAGE_TRANSFER_SRC_BIT,因为它将在之后进行Mipmap,Mipmap也是通过内存转移来实现的,在这个时候Texture将成为它的子Mipmap的转移来源。
ImageLayout的转移要通过命令来实现
这里对ImageLayout转移的实现放在了commandManager中,因为这是一个可以被反复使用的一个命令。
它的具体实现为:

void VKCommandManager::TransitionImageLayout(VkImage image, VkFormat format, uint32_t mipLevels , VkImageLayout oldLayout, VkImageLayout newLayout , VkQueue graphicsQueue, VkCommandPool commandPool)
{
	VkCommandBuffer commandBuffer = BeginSingleTimeCommand(commandPool);
	VkImageMemoryBarrier barrier = {};
	barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
	barrier.oldLayout = oldLayout;
	barrier.newLayout = newLayout;
	barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
	barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
	barrier.image = image;
	barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
	barrier.subresourceRange.baseMipLevel = 0;
	barrier.subresourceRange.levelCount = mipLevels;
	barrier.subresourceRange.baseArrayLayer = 0;
	barrier.subresourceRange.layerCount = 1;
	barrier.srcAccessMask = 0;
	barrier.dstAccessMask = 0;

	VkPipelineStageFlags sourceStage;
	VkPipelineStageFlags destinationStage;
	if (oldLayout == VK_IMAGE_LAYOUT_UNDEFINED && newLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL)
	{
		barrier.srcAccessMask = 0;
		barrier.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
		sourceStage = VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT;
		destinationStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
	}
	else if (oldLayout == VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL && newLayout == VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL)
	{
		barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
		barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
		sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
		destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;
	}
	else {
		throw std::invalid_argument("unsupported layout transition .");
	}

	vkCmdPipelineBarrier(commandBuffer, sourceStage, destinationStage, 0, 0, NULL, 0, NULL, 1, &barrier);

	EndSingleTimeCommand(commandBuffer , graphicsQueue , commandPool );
}

开头和结尾处的BeginSingleTimeCommand、EndSingleTimeCommand分别用来创建一个一次性使用的命令、把这个一次性使用的命令提交到队列中执行。具体的实现可以看源码,这里就不贴了。

现在来看函数的主体,这里不太好理解,ImageLayout的转移是通过一个VkImageMemoryBarrier来实现的,虽然Barrier是用来做同步的,但是它也有做Layout转移的一个功能,需要把旧的ImageLayout和新的ImageLayout传给Barrier,然后再通过vkCmdPipelineBarrier来完成Layout的转移,并且我们可以在vkCmdPipelineBarrier中指定ShaderStage来控制同步。比如在创建Texture时的调用:

	commandManager->TransitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM, mipLevels, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, queue, commandPool);

表示要将这个Image从VK_IMAGE_LAYOUT_UNDEFINED转移到VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,并且这个转移是不加限制的可以立即发生的(barrier.srcAccessMask = 0 表示不需要等待任何操作的结束就可以执行)

	commandManager->TransitionImageLayout(textureImage, VK_FORMAT_R8G8B8A8_UNORM, mipLevels, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, queue, commandPool );

表示要将这个Image从VK_IMAGE从VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL(拷贝结束时的Layout)转移到VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,并且这个转移必须要在fragment shader读取它之前完成,因为这里会分支到

		barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
		barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;
		sourceStage = VK_PIPELINE_STAGE_TRANSFER_BIT;
		destinationStage = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT;

barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT表示在fragment shader读取它之前要完成:barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT即转移时写入(即拷贝完成)

创建ImageView

直接先贴代码:

VkImageView VKResourceCreator::CreateImageView(VkImage image, VkFormat format, VkImageAspectFlags aspectFlags, uint32_t mipLevels)
{
	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 = aspectFlags;
	viewInfo.subresourceRange.baseMipLevel = 0;
	viewInfo.subresourceRange.levelCount = mipLevels;
	viewInfo.subresourceRange.baseArrayLayer = 0;
	viewInfo.subresourceRange.layerCount = 1;

	VkImageView imageView;
	if (vkCreateImageView(logical_device_, &viewInfo, NULL, &imageView) != VK_SUCCESS)
	{
		throw std::runtime_error("failed to create texture image view .");
	}

	return imageView;
}

和第二节中创建ImageView基本相同,只是改了一下 viewInfo.subresourceRange.levelCount=mipLevels,因为之后会生成Mipmaps,这里就指的是Mipmap会生成的图像的数量。

创建Sampler

Sampler这个概念我就不解释了,能看到这里的应该都明白吧……

VkSampler VKResourceCreator::CreateSampler(uint32_t mipLevels)
{
	VkSamplerCreateInfo samplerInfo = {};
	samplerInfo.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO;
	samplerInfo.magFilter = VK_FILTER_LINEAR;
	samplerInfo.minFilter = VK_FILTER_LINEAR;

	samplerInfo.addressModeU = VK_SAMPLER_ADDRESS_MODE_REPEAT;
	samplerInfo.addressModeV = VK_SAMPLER_ADDRESS_MODE_REPEAT;
	samplerInfo.addressModeW = VK_SAMPLER_ADDRESS_MODE_REPEAT;
	samplerInfo.anisotropyEnable = VK_TRUE;
	samplerInfo.maxAnisotropy = 16;
	samplerInfo.borderColor = VK_BORDER_COLOR_INT_OPAQUE_BLACK;
	samplerInfo.unnormalizedCoordinates = VK_FALSE;
	samplerInfo.compareEnable = VK_FALSE;
	samplerInfo.compareOp = VK_COMPARE_OP_ALWAYS;
	samplerInfo.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR;
	samplerInfo.mipLodBias = 0.0f;
	samplerInfo.minLod = 0;
	samplerInfo.maxLod = mipLevels;

	VkSampler sampler;
	if (vkCreateSampler(logical_device_, &samplerInfo, NULL, &sampler) != VK_SUCCESS)
	{
		throw std::runtime_error("failed to create texture sampler.");
	}

	return sampler;
}

这些和DX基本类似,不多说了。

绑定Image和Sampler到管线上

上一节在绑定UniformBuffer时,使用的是一些Descriptor来完成,绑定Image和Sampler同样也需要Descriptor,先来看fragment shader的修改:

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(binding = 1) uniform sampler2D texSampler;

layout(location = 0) in vec3 fragColor;
layout(location = 1) in vec2 fragTexCoord;

layout(location = 0) out vec4 outColor;

void main() {
    outColor = texture(texSampler, fragTexCoord);
}

可以看到的这里只绑定了一个sampler,因为这里会将sampler和Image混合在一起绑定:

	VkDescriptorSetLayoutBinding uboLayoutBinding = {};
	uboLayoutBinding.binding = 0;
	uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
	uboLayoutBinding.descriptorCount = 1;
	uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
	uboLayoutBinding.pImmutableSamplers = NULL;

	VkDescriptorSetLayoutBinding samplerLayoutBinding = {};
	samplerLayoutBinding.binding = 1;
	samplerLayoutBinding.descriptorCount = 1;
	samplerLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
	samplerLayoutBinding.pImmutableSamplers = nullptr;
	samplerLayoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;

新加入了一个VkDescriptorSetLayoutBinding,它的类型为: VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,注意它是COMBINED_IMAGE_SAMPLER,说明Image和Sampler是一起绑定的,最终在fragment shader中就只体现为一个layout(binding = 1) uniform sampler2D texSampler;

然后在将实际的资源与Descriptor绑定的地方做修改:

	for (size_t i = 0; i < swapchain_image_views_.size(); i++)
	{
		VkDescriptorBufferInfo bufferInfo = {};
		bufferInfo.buffer = uniform_buffer_[i];
		bufferInfo.offset = 0;
		bufferInfo.range = sizeof(UniformBufferObject);

		VkDescriptorImageInfo imageInfo = {};
		imageInfo.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
		imageInfo.imageView = texture_image_view_;
		imageInfo.sampler = texture_sampler_;

		std::array descriptorWrites = {};

		descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
		descriptorWrites[0].dstSet = descriptorSets[i];
		descriptorWrites[0].dstBinding = 0;
		descriptorWrites[0].dstArrayElement = 0;
		descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
		descriptorWrites[0].descriptorCount = 1;
		descriptorWrites[0].pBufferInfo = &bufferInfo;

		descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
		descriptorWrites[1].dstSet = descriptorSets[i];
		descriptorWrites[1].dstBinding = 1;
		descriptorWrites[1].dstArrayElement = 0;
		descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
		descriptorWrites[1].descriptorCount = 1;
		descriptorWrites[1].pImageInfo = &imageInfo;

		vkUpdateDescriptorSets(logical_device_ , static_cast(descriptorWrites.size()), descriptorWrites.data(), 0, nullptr);
	}

可以看到这里用到的DescriptorInfo类型是和Buffer不一样的VkDescriptorImageInfo,VkDescriptorImageInfo会指定一个ImageView和Sampler,然后把它传递给VkWriteDescriptorSet,最终就完成Descriptor和Image、Sampler的绑定了,完整代码有点长就不贴了,大体的思路和绑定UniformBuffer是一致的,只是需要做细微的修改,详情可参考源码。

加载模型

由于使用到了纹理,我们需要对Vertex的定义以及VertexShader做一定的修改:

struct Vertex {
	float pos[3];
	float color[3];
	float texCoord[2];
	static VkVertexInputBindingDescription getBindingDescription() {
		VkVertexInputBindingDescription bindingDescription = {};
		bindingDescription.binding = 0;
		bindingDescription.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;
		bindingDescription.stride = sizeof(Vertex);
		return bindingDescription;
	}

	static std::array getAttributeDescription() {
		std::array attributeDescriptions = {};
		attributeDescriptions[0].binding = 0;
		attributeDescriptions[0].location = 0;
		attributeDescriptions[0].format = VK_FORMAT_R32G32B32_SFLOAT;
		attributeDescriptions[0].offset = offsetof(Vertex, pos);

		attributeDescriptions[1].binding = 0;
		attributeDescriptions[1].location = 1;
		attributeDescriptions[1].format = VK_FORMAT_R32G32B32_SFLOAT;
		attributeDescriptions[1].offset = offsetof(Vertex, color);

		attributeDescriptions[2].binding = 0;
		attributeDescriptions[2].location = 2;
		attributeDescriptions[2].format = VK_FORMAT_R32G32_SFLOAT;
		attributeDescriptions[2].offset = offsetof(Vertex, texCoord);

		return attributeDescriptions;
	}
};

#version 450
#extension GL_ARB_separate_shader_objects : enable

layout(binding = 0) uniform UniformBufferObject {
    mat4 model;
    mat4 view;
    mat4 proj;
} ubo;

layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 2) in vec2 inTexCoord;

layout(location = 0) out vec3 fragColor;
layout(location = 1) out vec2 fragTexCoord;

void main() {
    gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 1.0);
    fragColor = inColor;
    fragTexCoord = inTexCoord;
}

然后把Vertex、Index的硬编码数据改为从模型中读取:

void VKResourceCreator::LoadMeshData(const char* file, std::vector& vertices, std::vector& indices)
{
	tinyobj::attrib_t attrib;
	std::vector shapes;
	std::vector materials;
	std::string warn, err;

	if (!tinyobj::LoadObj(&attrib, &shapes, &materials, &warn, &err, file))
	{
		throw std::runtime_error(warn + err);
	}

	for (const auto& shape : shapes)
	{
		for (const auto& index : shape.mesh.indices)
		{
			Vertex vertex = {};
			vertex.pos[0] = attrib.vertices[3 * index.vertex_index + 0];
			vertex.pos[1] = attrib.vertices[3 * index.vertex_index + 1];
			vertex.pos[2] = attrib.vertices[3 * index.vertex_index + 2];

			vertex.texCoord[0] = attrib.texcoords[2 * index.texcoord_index + 0];
			vertex.texCoord[1] = 1.0f - attrib.texcoords[2 * index.texcoord_index + 1];

			vertex.color[0] = 1.0f;
			vertex.color[1] = 1.0f;
			vertex.color[2] = 1.0f;


			vertices.push_back(vertex);
			indices.push_back(indices.size());
		}
	}
}

这样就能实现把模型数据都加载进来了,程序比较简单,不须多做解释。

生成Mipmap

Vulkan中生成Mipmap要比DX中麻烦很多,每一个Mipmap的生成都需要手动来完成,在创建完Image和ImageView后调用:

	command_manager_->GenerateMipmaps(texture_image_, VK_FORMAT_R8G8B8A8_UNORM, texWidth, texHeight, mipLevels, queue , command_pool_ );

来看GenerateMipmaps的具体实现:

void VKCommandManager::GenerateMipmaps(VkImage image, VkFormat imageFormat, int32_t texWidth, int32_t texHeight, uint32_t mipLevels,  VkQueue graphicsQueue, VkCommandPool commandPool)
{
	VkCommandBuffer commandBuffer = BeginSingleTimeCommand( commandPool );
	VkImageMemoryBarrier barrier = {};
	barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
	barrier.image = image;
	barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
	barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
	barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
	barrier.subresourceRange.baseArrayLayer = 0;
	barrier.subresourceRange.layerCount = 1;
	barrier.subresourceRange.levelCount = 1;

	int32_t mipWidth = texWidth;
	int32_t mipHeight = texHeight;

	for (uint32_t i = 1; i < mipLevels; i++)
	{
		barrier.subresourceRange.baseMipLevel = i - 1;
		barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
		barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
		barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
		barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT;

		vkCmdPipelineBarrier(commandBuffer,
			VK_PIPELINE_STAGE_TRANSFER_BIT,
			VK_PIPELINE_STAGE_TRANSFER_BIT,
			0,
			0,
			NULL,
			0,
			NULL,
			1,
			&barrier
		);

		VkImageBlit blit = {};
		blit.srcOffsets[0] = { 0 , 0 , 0 };
		blit.srcOffsets[1] = { mipWidth , mipHeight , 1 };
		blit.srcSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
		blit.srcSubresource.mipLevel = i - 1;
		blit.srcSubresource.baseArrayLayer = 0;
		blit.srcSubresource.layerCount = 1;
		blit.dstOffsets[0] = { 0 , 0 , 0 };
		blit.dstOffsets[1] = { mipWidth > 1 ? mipWidth / 2 : 1 , mipHeight > 1 ? mipHeight / 2 : 1 , 1 };
		blit.dstSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
		blit.dstSubresource.mipLevel = i;
		blit.dstSubresource.baseArrayLayer = 0;
		blit.dstSubresource.layerCount = 1;

		vkCmdBlitImage(commandBuffer,
			image,
			VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,
			image,
			VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
			1,
			&blit,
			VK_FILTER_LINEAR
		);

		barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL;
		barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
		barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT;
		barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

		vkCmdPipelineBarrier(commandBuffer,
			VK_PIPELINE_STAGE_TRANSFER_BIT,
			VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
			0, NULL,
			0, NULL,
			1, &barrier);

		if (mipWidth > 1) mipWidth /= 2;
		if (mipHeight > 1) mipHeight /= 2;
	}

	barrier.subresourceRange.baseMipLevel = mipLevels - 1;
	barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
	barrier.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
	barrier.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
	barrier.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

	vkCmdPipelineBarrier(commandBuffer,
		VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0,
		0, NULL,
		0, NULL,
		1, &barrier
	);

	EndSingleTimeCommand(commandBuffer , graphicsQueue , commandPool );
}

在生成每个Mipmap时,都需要用到前一个Mipmap,拿它来做降采样,然后就能获得长度和宽度均为前一个mipmap 1/2的图像,降采样也是一个内存转移类指令。因此我们需要通过一个循环来生成Mipmap,在每一次循环中,要操作相邻的两个Mipmap,编号分别为i-1和i,i-1号Mipmap必须要被放到VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL,因此需要做一次Layout转移,但是在生成Texture时已经将纹理从VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL转移到VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT了,对于已经通过Transfer转移到VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT的纹理而言,它们是不能够再被转回去的。因此如果要生成Mipmap,则必须跳过创建纹理时的第六步,即将纹理从VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL转移到VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT这一步去掉,让Image停留在VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL。

转移好i-1号Mipmap的Layout后,就可以执行命令vkCmdBlitImage来完成降采样,这些参数都比较直观,不多说明。

然后在每次降采样完成后要将第i-1号Mipmap再转移到VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT供FragmentShader使用,这就是生成Mipmap的全过程了。

在创建ImageView中指定的:

	viewInfo.subresourceRange.baseMipLevel = 0;
	viewInfo.subresourceRange.levelCount = mipLevels;

可以控制使用的Mipmap的范围,然后在运行时,会根据LOD自动选择合适的Mipmap,这样既能提升性能,也能减少图像的走样。

这篇写的比较仓促,很多地方可能没说的特别清楚,如果有疑问和错误请及时指出!

你可能感兴趣的:(Vulkan)