Vulkan学习:立方体贴图与天空盒

前言

立方体贴图是一种特殊的贴图,它可以用来实现天空盒技术。
在这里插入图片描述
天空盒可以看作一个很大的立方体,相机被包裹在这个立方体之内,只会渲染立方体内部的表面。由于天空盒很大,所以无论相机在哪个位置观察,都会感觉天空盒和自己的距离一直保持不变。
场景中的物体还可以向天空盒发起采样,然后把采样到的颜色显示在自己的表面,就可以形成像上图一样的反射效果。
本篇将分析SaschaWillems的texturecubemap案例的代码实现思路,然后在自己的小引擎上也尝试复现。

资源准备

下图是样例中自带的用于展示天空盒的Cube,它是gltf格式的。可以看到,它的面光影的显示存在一定的问题,可能是法线设定得不太正确。同时,模型的rotation不为0,我认为这是不规范的。把模型导入blender之后,发现该模型在blender中的边长正好是10m,因此我决定自己在blender中导出一个更加规范的10m边长的天空盒供展示使用。
在这里插入图片描述
本项目的天空盒是ktx格式的,使用ktx库读取资源。
在这里插入图片描述
ktx库的代码可以在这个路径下找到,这是一个只有源码的库,所以我需要把它的头文件和部分源文件都加入我自己的项目。
头文件加入项目的方式很简单,有cmake基础的人都知道该如何做。这里注意,需要把include和lib这两个文件夹都加入项目的头文件查找目录,因为二者里面都有.h文件。
至于源文件,哪些源文件需要加入项目呢?通过查询原程序的工程结构,我发现,是以下几个文件:

files
	{
	    //项目自带的文件
		"src/**.h",
		"src/**.cpp",
		"vendor/stb_image/**.h",
		"vendor/stb_image/**.cpp",
		"vendor/glm/glm/**.hpp",
		"vendor/glm/glm/**.inl",

		"vendor/ImGuizmo/ImGuizmo.h",
		"vendor/ImGuizmo/ImGuizmo.cpp",
		......
		//ktx库中需要加入项目的.c文件
		"vendor/ktx/lib/texture.c",
		"vendor/ktx/lib/hashlist.c",
		"vendor/ktx/lib/checkheader.c",
		"vendor/ktx/lib/swap.c",
		"vendor/ktx/lib/memstream.c",
		"vendor/ktx/lib/filestream.c",
	}

我的项目使用premake管理,所以以上代码是premake的。但cmake管理项目也是一样的思路,不需要过多介绍。

以下链接介绍了一种将hdr格式转为ktx的工具,还介绍了一个可以下载hdr格式天空盒的网站。
cmgen制作天空盒

代码分析

总体流程

以下是该项目准备阶段的代码。准备阶段的主要工作是导入模型、贴图,准备UniformBuffer、描述符集、渲染管线等资源。在最后一步buildCommandBuffers()函数中,程序把所有绘制天空盒和模型有关的命令都提前放在了CommandBuffer中。由于本项目不需要频繁更改CommandBuffer,所以在绘制阶段,只需要不断地把之前准备阶段CommandBuffer中记录好的命令反复提交给GPU渲染即可。
当然,如果某些重要参数发生了更改,还是需要重建CommandBuffer的。

	void prepare()
	{
		VulkanExampleBase::prepare();
		loadAssets();
		prepareUniformBuffers();
		setupDescriptorSetLayout();
		preparePipelines();
		setupDescriptorPool();
		setupDescriptorSets();
		buildCommandBuffers();
		prepared = true;
	}

以下是该项目绘制阶段和UI更新阶段的代码。这两个函数每一帧调用一次,UI更新的函数会监听一些配置参数的改变,在合适的时候重建CommandBuffer。

void draw()
{
	VulkanExampleBase::prepareFrame();
	submitInfo.commandBufferCount = 1;
	submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];
	VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, VK_NULL_HANDLE));
	VulkanExampleBase::submitFrame();
}

virtual void OnUpdateUIOverlay(vks::UIOverlay *overlay)
{
	if (overlay->header("Settings")) {
		if (overlay->sliderFloat("LOD bias", &uboVS.lodBias, 0.0f, (float)cubeMap.mipLevels)) {
			updateUniformBuffers();
		}
		if (overlay->comboBox("Object type", &models.objectIndex, objectNames)) {
			buildCommandBuffers();
		}
		if (overlay->checkBox("Skybox", &displaySkybox)) {
			buildCommandBuffers();
		}
	}
}

模型和贴图加载

void loadAssets()
{
	uint32_t glTFLoadingFlags = vkglTF::FileLoadingFlags::PreTransformVertices | vkglTF::FileLoadingFlags::FlipY;
	// Skybox
	models.skybox.loadFromFile(getAssetPath() + "models/cube.gltf", vulkanDevice, queue, glTFLoadingFlags);
	// Objects
	std::vector<std::string> filenames = { "sphere.gltf", "teapot.gltf", "torusknot.gltf", "venus.gltf" };
	objectNames = { "Sphere", "Teapot", "Torusknot", "Venus" };
	models.objects.resize(filenames.size());
	for (size_t i = 0; i < filenames.size(); i++) {
		models.objects[i].loadFromFile(getAssetPath() + "models/" + filenames[i], vulkanDevice, queue, glTFLoadingFlags);
	}
	// Cubemap texture
	const bool forceLinearTiling = false;
	loadCubemap(getAssetPath() + "textures/cubemap_yokohama_rgba.ktx", VK_FORMAT_R8G8B8A8_UNORM, forceLinearTiling);
}

以上代码为加载gltf格式模型的代码和加载CubeMap的代码。加载gltf格式模型的代码并不是我要关注的重点,因为我的项目是使用TinyObjLoader加载obj格式模型的。接下来重点分析以下加载CubeMap的代码。这段代码比较长,不过其实大部分都是配置的信息,并不难以理解,我将会在注释中标出要重点关注的内容。

		if (!vks::tools::fileExists(filename)) {
			vks::tools::exitFatal("Could not load texture from " + filename + "\n\nMake sure the assets submodule has been checked out and is up-to-date.", -1);
		}
		//1.从给定的文件名中加载ktx格式的贴图
		result = ktxTexture_CreateFromNamedFile(filename.c_str(), KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktxTexture);
		
		assert(result == KTX_SUCCESS);
        //2.从ktx纹理中获取宽度,高度,mipLevel等参数,以及获取纹理中的数据,在后面的步骤中会把这些数据拷贝到staging buffer,然后从staging buffer中拷贝到vulkan的image中
		// Get properties required for using and upload texture data from the ktx texture object
		cubeMap.width = ktxTexture->baseWidth;
		cubeMap.height = ktxTexture->baseHeight;
		cubeMap.mipLevels = ktxTexture->numLevels;
		ktx_uint8_t *ktxTextureData = ktxTexture_GetData(ktxTexture);
		ktx_size_t ktxTextureSize = ktxTexture_GetSize(ktxTexture);

		VkMemoryAllocateInfo memAllocInfo = vks::initializers::memoryAllocateInfo();
		VkMemoryRequirements memReqs;

        //3.创建stagingBuffer,准备写入贴图数据
		// Create a host-visible staging buffer that contains the raw image data
		VkBuffer stagingBuffer;
		VkDeviceMemory stagingMemory;

		VkBufferCreateInfo bufferCreateInfo = vks::initializers::bufferCreateInfo();
		bufferCreateInfo.size = ktxTextureSize;
		// This buffer is used as a transfer source for the buffer copy
		bufferCreateInfo.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
		bufferCreateInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE;

		VK_CHECK_RESULT(vkCreateBuffer(device, &bufferCreateInfo, nullptr, &stagingBuffer));

		// Get memory requirements for the staging buffer (alignment, memory type bits)
		vkGetBufferMemoryRequirements(device, stagingBuffer, &memReqs);
		memAllocInfo.allocationSize = memReqs.size;
		// Get memory type index for a host visible buffer
		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));

        //4.准备把贴图数据送入staging buffer。该过程是一个cpu上的过程,所以不需要command buffer来完成
		// Copy texture data into staging buffer
		uint8_t *data;
		VK_CHECK_RESULT(vkMapMemory(device, stagingMemory, 0, memReqs.size, 0, (void **)&data));
		memcpy(data, ktxTextureData, ktxTextureSize);
		vkUnmapMemory(device, stagingMemory);

        //5.创建真正存储贴图数据的Image对象
		// Create optimal tiled target image
		VkImageCreateInfo imageCreateInfo = vks::initializers::imageCreateInfo();
		imageCreateInfo.imageType = VK_IMAGE_TYPE_2D;
		imageCreateInfo.format = format;
		imageCreateInfo.mipLevels = cubeMap.mipLevels;//这里需要和之前的mipLevel保持一致
		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 = { cubeMap.width, cubeMap.height, 1 };//这里需要和之前的宽度,高度保持一致
		imageCreateInfo.usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
		// Cube faces count as array layers in Vulkan
		imageCreateInfo.arrayLayers = 6;
		// This flag is required for cube map images
		imageCreateInfo.flags = VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT;

		VK_CHECK_RESULT(vkCreateImage(device, &imageCreateInfo, nullptr, &cubeMap.image));

		vkGetImageMemoryRequirements(device, cubeMap.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, &cubeMap.deviceMemory));
		VK_CHECK_RESULT(vkBindImageMemory(device, cubeMap.image, cubeMap.deviceMemory, 0));

        //6.创建一个command buffer,把staging buffer中的数据拷贝到image中
		VkCommandBuffer copyCmd = vulkanDevice->createCommandBuffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY, true);

		// Setup buffer copy regions for each face including all of its miplevels
		std::vector<VkBufferImageCopy> bufferCopyRegions;
		uint32_t offset = 0;

		for (uint32_t face = 0; face < 6; face++)
		{
			for (uint32_t level = 0; level < cubeMap.mipLevels; level++)
			{
				// Calculate offset into staging buffer for the current mip level and face
				ktx_size_t offset;
				KTX_error_code ret = ktxTexture_GetImageOffset(ktxTexture, level, 0, face, &offset);
				assert(ret == KTX_SUCCESS);
				VkBufferImageCopy bufferCopyRegion = {};
				bufferCopyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
				bufferCopyRegion.imageSubresource.mipLevel = level;
				bufferCopyRegion.imageSubresource.baseArrayLayer = face;
				bufferCopyRegion.imageSubresource.layerCount = 1;
				bufferCopyRegion.imageExtent.width = ktxTexture->baseWidth >> level;//每一个新的mipLevel,宽度和高度都是上一level的一半
				bufferCopyRegion.imageExtent.height = ktxTexture->baseHeight >> level;
				bufferCopyRegion.imageExtent.depth = 1;
				bufferCopyRegion.bufferOffset = offset;
				bufferCopyRegions.push_back(bufferCopyRegion);
			}
		}
        //7.Image进行状态转换,先转换为拷贝数据的状态,在拷贝完数据之后,再转换为可以被shader读取的状态
		// Image barrier for optimal image (target)
		// Set initial layout for all array layers (faces) of the optimal (target) tiled texture
		VkImageSubresourceRange subresourceRange = {};
		subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
		subresourceRange.baseMipLevel = 0;
		subresourceRange.levelCount = cubeMap.mipLevels;
		subresourceRange.layerCount = 6;

		vks::tools::setImageLayout(
			copyCmd,
			cubeMap.image,
			VK_IMAGE_LAYOUT_UNDEFINED,
			VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
			subresourceRange);

		// Copy the cube map faces from the staging buffer to the optimal tiled image
		vkCmdCopyBufferToImage(
			copyCmd,
			stagingBuffer,
			cubeMap.image,
			VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
			static_cast<uint32_t>(bufferCopyRegions.size()),
			bufferCopyRegions.data()
			);

		// Change texture image layout to shader read after all faces have been copied
		cubeMap.imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;
		vks::tools::setImageLayout(
			copyCmd,
			cubeMap.image,
			VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
			cubeMap.imageLayout,
			subresourceRange);

		vulkanDevice->flushCommandBuffer(copyCmd, queue, true);
        //8.创建Sampler对象和Image View对象
		// Create sampler
		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_CLAMP_TO_EDGE;
		sampler.addressModeV = sampler.addressModeU;
		sampler.addressModeW = sampler.addressModeU;
		sampler.mipLodBias = 0.0f;
		sampler.compareOp = VK_COMPARE_OP_NEVER;
		sampler.minLod = 0.0f;
		sampler.maxLod = static_cast<float>(cubeMap.mipLevels);
		sampler.borderColor = VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE;
		sampler.maxAnisotropy = 1.0f;
		if (vulkanDevice->features.samplerAnisotropy)
		{
			sampler.maxAnisotropy = vulkanDevice->properties.limits.maxSamplerAnisotropy;
			sampler.anisotropyEnable = VK_TRUE;
		}
		VK_CHECK_RESULT(vkCreateSampler(device, &sampler, nullptr, &cubeMap.sampler));

		// Create image view
		VkImageViewCreateInfo view = vks::initializers::imageViewCreateInfo();
		// Cube map view type
		view.viewType = VK_IMAGE_VIEW_TYPE_CUBE;//指定是cube类型的Image View
		view.format = format;
		view.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
		// 6 array layers (faces)
		view.subresourceRange.layerCount = 6;
		// Set number of mip levels
		view.subresourceRange.levelCount = cubeMap.mipLevels;
		view.image = cubeMap.image;
		VK_CHECK_RESULT(vkCreateImageView(device, &view, nullptr, &cubeMap.view));
        //9.清除没有用的中间资源
		// Clean up staging resources
		vkFreeMemory(device, stagingMemory, nullptr);
		vkDestroyBuffer(device, stagingBuffer, nullptr);
		ktxTexture_Destroy(ktxTexture);

整个过程和Vulkan加载普通纹理图像的过程非常相似。这一切都加载完毕后,我们就可以把CubeMap的描述符写入需要的描述符集中,然后绑定描述符集,就可以在Shader中访问了。

渲染资源

渲染天空盒所需要的资源很简单,只有一个ViewProjection矩阵和一个立方体贴图用于采样。
ViewProjection矩阵需要把移动部分清零,这是因为,无论相机移动到什么位置,场景都应该看起来始终在天空盒的中心,就像现实中的天空一样,无论如何尝试靠近它,都始终会感觉它距离我们很远。
天空盒的渲染管线配置和默认的管线配置基本一样,只需要注意两点:

  1. 天空盒需要正面剔除而不是背面剔除;
  2. 天空盒关闭深度测试和深度写入。

绘制流程

天空盒需要绘制在场景中所有物体的前面,且不参与阴影贴图的生成。先绑定天空盒的管线,绘制天空盒,然后再正常绘制其他物体即可。

个人尝试

这是我把天空盒渲染部分整合到自己引擎之后的渲染尝试。渲染效果和原程序基本相同。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值