Link

让我们开始编写代码,在引擎上上传和处理纹理。

我们将从创建另一对文件开始。就像我们对网格所做的那样。vk_textures.h 和 .cpp。我们将把加载纹理的主要代码存储在那里。

vk_textures.h

#pragma once

#include <vk_types.h>
#include <vk_engine.h>

namespace vkutil {

	bool load_image_from_file(VulkanEngine& engine, const char* file, AllocatedImage& outImage);

}

我们将有一个单一的 load-image 函数,它将执行从磁盘文件到 GPU 内存的整个加载操作。

我们将使用 stb_image 库来加载纹理。

在 vk_textures.cpp 中,我们开始填充函数。

#include <vk_textures.h>
#include <iostream>

#include <vk_initializers.h>

#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>

bool vkutil::load_image_from_file(VulkanEngine& engine, const char* file, AllocatedImage& outImage)
{
	int texWidth, texHeight, texChannels;

	stbi_uc* pixels = stbi_load(file, &texWidth, &texHeight, &texChannels, STBI_rgb_alpha);

	if (!pixels) {
		std::cout << "Failed to load texture file " << file << std::endl;
		return false;
	}

	return true;
}

在开始时,我们使用 stbi_load() 直接从文件将纹理加载到 CPU 像素数组中。如果找不到文件或出现错误,该函数将返回 nullptr。加载函数时,我们还将 STBI_rgb_alpha 发送到函数,这将使库始终将像素加载为 RGBA 4 通道。这很有用,因为它将与我们用于 Vulkan 的格式匹配。

纹理文件加载到像素数组后,我们可以创建一个暂存缓冲区并将像素存储在那里。这几乎与我们在上一篇文章中将网格复制到 GPU 时所做的相同。

	void* pixel_ptr = pixels;
	VkDeviceSize imageSize = texWidth * texHeight * 4;

	//the format R8G8B8A8 matches exactly with the pixels loaded from stb_image lib
	VkFormat image_format = VK_FORMAT_R8G8B8A8_SRGB;

	//allocate temporary buffer for holding texture data to upload
	AllocatedBuffer stagingBuffer = engine.create_buffer(imageSize, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_ONLY);

	//copy data to buffer
	void* data;
	vmaMapMemory(engine._allocator, stagingBuffer._allocation, &data);

	memcpy(data, pixel_ptr, static_cast<size_t>(imageSize));

	vmaUnmapMemory(engine._allocator, stagingBuffer._allocation);
	//we no longer need the loaded data, so we can free the pixels as they are now in the staging buffer
	stbi_image_free(pixels);

我们通过每个像素 4 个字节和 texWidth * texHeight 像素数来计算图像大小。对于格式,我们将其与 stb 格式 STBI_rgb_alpha 匹配。然后我们创建一个缓冲区来保存该数据并将其复制到那里。一旦数据被复制到缓冲区中,我们就不再需要原始库加载的像素,因此我们可以释放它。

我们现在继续创建图像

VkExtent3D imageExtent;
	imageExtent.width = static_cast<uint32_t>(texWidth);
	imageExtent.height = static_cast<uint32_t>(texHeight);
	imageExtent.depth = 1;

	VkImageCreateInfo dimg_info = vkinit::image_create_info(image_format, VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, imageExtent);

	AllocatedImage newImage;

	VmaAllocationCreateInfo dimg_allocinfo = {};
	dimg_allocinfo.usage = VMA_MEMORY_USAGE_GPU_ONLY;

	//allocate and create the image
	vmaCreateImage(engine._allocator, &dimg_info, &dimg_allocinfo, &newImage._image, &newImage._allocation, nullptr);

这与我们创建深度图像时类似。主要区别在于图像的使用标志,它将是 Sampled 和 Transfer Destination,因为我们将此用作着色器的纹理。我们使用 VMA 和 GPU_ONLY 内存类型,以便图像分配在 VRAM 上

创建图像并准备好缓冲区后,我们现在可以开始将数据复制到其中的命令。

对于图像,您不能直接将数据从缓冲区复制到图像中。现在,图像未在任何特定布局中初始化,因此我们需要进行布局转换,以便驱动程序将纹理置于线性布局中,这最适合将数据从缓冲区复制到纹理。

要进行布局转换,我们这样做

engine.immediate_submit([&](VkCommandBuffer cmd) {
		VkImageSubresourceRange range;
		range.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
		range.baseMipLevel = 0;
		range.levelCount = 1;
		range.baseArrayLayer = 0;
		range.layerCount = 1;

		VkImageMemoryBarrier imageBarrier_toTransfer = {};
		imageBarrier_toTransfer.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;

		imageBarrier_toTransfer.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
		imageBarrier_toTransfer.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
		imageBarrier_toTransfer.image = newImage._image;
		imageBarrier_toTransfer.subresourceRange = range;

		imageBarrier_toTransfer.srcAccessMask = 0;
		imageBarrier_toTransfer.dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;

		//barrier the image into the transfer-receive layout
		vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, nullptr, 0, nullptr, 1, &imageBarrier_toTransfer);
});

要执行布局转换,我们需要使用管线屏障。管线屏障可以控制 GPU 如何重叠屏障之前和之后的命令,但是如果您使用图像屏障执行管线屏障,则驱动程序还可以将图像转换为正确的格式和布局。

在这里,我们从 VkImageSubresourceRange 开始,以告诉它我们将转换图像的哪个部分。在这里,我们没有 mipmap 或分层纹理,因此我们将级别计数和图层计数都设置为 1。

接下来,我们用布局转换填充 VkImageMemoryBarrier。我们从布局 VK_IMAGE_LAYOUT_UNDEFINED 开始,然后转到 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL。这会将图像准备为布局,以便作为内存传输的目标。

我们也指向图像和子资源范围。

填充完成后,我们可以执行管线屏障。它将是一个从 VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT 阻塞到 VK_PIPELINE_STAGE_TRANSFER_BIT 的屏障。如果您想了解有关管线屏障阶段的确切细节,请阅读此内容。https://gpuopen.com/learn/vulkan-barriers-explained/ 对于我们正在做的事情,我们不需要知道这里的具体细节。

图像准备好接收像素数据后,我们现在可以使用命令进行传输。我们继续填充 immediate_submit() 调用

	VkBufferImageCopy copyRegion = {};
	copyRegion.bufferOffset = 0;
	copyRegion.bufferRowLength = 0;
	copyRegion.bufferImageHeight = 0;

	copyRegion.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
	copyRegion.imageSubresource.mipLevel = 0;
	copyRegion.imageSubresource.baseArrayLayer = 0;
	copyRegion.imageSubresource.layerCount = 1;
	copyRegion.imageExtent = imageExtent;

	//copy the buffer into the image
	vkCmdCopyBufferToImage(cmd, stagingBuffer._buffer, newImage._image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &copyRegion);

就像我们复制缓冲区时一样,我们需要填充一个结构,其中包含要复制的信息。我们的缓冲区偏移量为 0,然后是要复制到的确切图层和 mipmap,即级别 0 和 1 图层。我们还需要发送 imageExtent 以获取图像大小。

我们现在执行 VkCmdCopyBufferToImage() 命令,其中我们还需要指定图像的布局,即 TRANSFER_DST_OPTIMAL

图像现在具有正确的像素数据,因此我们可以再次更改其布局,使其成为着色器可读的布局。

	VkImageMemoryBarrier imageBarrier_toReadable = imageBarrier_toTransfer;

	imageBarrier_toReadable.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL;
	imageBarrier_toReadable.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL;

	imageBarrier_toReadable.srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT;
	imageBarrier_toReadable.dstAccessMask = VK_ACCESS_SHADER_READ_BIT;

	//barrier the image into the shader readable layout
	vkCmdPipelineBarrier(cmd, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, 0, nullptr, 0, nullptr, 1, &imageBarrier_toReadable);

我们将图像传输到 VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,以使驱动程序知道将图像混洗为最适合从着色器读取的任何内部格式。

现在,我们通过释放我们不需要的资源并将图像添加到删除队列来完成加载函数。

bool vkutil::load_image_from_file(VulkanEngine& engine, const char* file, AllocatedImage & outImage)
{
	// file load code

	// staging buffer copy

	engine.immediate_submit([&](VkCommandBuffer cmd) {
		//transitions and copy commands
	});


	engine._mainDeletionQueue.push_function([=]() {

		vmaDestroyImage(engine._allocator, newImage._image, newImage._allocation);
	});

	vmaDestroyBuffer(engine._allocator, stagingBuffer._buffer, stagingBuffer._allocation);

	std::cout << "Texture loaded successfully " << file << std::endl;

	outImage = newImage;
	return true;
}

我们现在可以将许多文件格式(如 .jpeg 和 .png)加载到纹理中。让我们尝试使用 assets 文件夹中的一个文件,以确保它有效。

让我们添加一种在 VulkanEngine 上加载和存储图像的方法

vulkan_engine.h

struct Texture {
	AllocatedImage image;
	VkImageView imageView;
};

class VulkanEngine {
public:
//texture hashmap
std::unordered_map<std::string, Texture> _loadedTextures;

void load_images();
}

vk_engine.cpp

//add the textures.h include
#include "vk_textures.h"

void VulkanEngine::load_images()
{
	Texture lostEmpire;

	vkutil::load_image_from_file(*this, "../../assets/lost_empire-RGBA.png", lostEmpire.image);

	VkImageViewCreateInfo imageinfo = vkinit::imageview_create_info(VK_FORMAT_R8G8B8A8_SRGB, lostEmpire.image._image, VK_IMAGE_ASPECT_COLOR_BIT);
	vkCreateImageView(_device, &imageinfo, nullptr, &lostEmpire.imageView);

	_loadedTextures["empire_diffuse"] = lostEmpire;
}

确保在 VulkanEngine::init() 函数中,在 init_scene() 之前调用 load_images()

void VulkanEngine::init()
{
	// other code ....

	load_images();

	load_meshes();

	init_scene();
	//everything went fine
	_isInitialized = true;

现在纹理已加载,我们需要将其显示到着色器中,并重构一些内容。

下一步:绘制图像