Link

当我们进行基于计算的渲染时,我们已经展示了如何使用图像,但是关于图像,我们仍然需要处理一些事情,特别是如何在图形着色器中使用它们进行渲染和显示。我们将从这里开始,为我们的引擎创建一组默认纹理,然后从文件中加载纹理。

首先,我们需要向 VulkanEngine 类添加函数来处理创建和销毁图像。

将这些函数添加到头文件中的类中。

class VulkanEngine {

AllocatedImage create_image(VkExtent3D size, VkFormat format, VkImageUsageFlags usage, bool mipmapped = false);
AllocatedImage create_image(void* data, VkExtent3D size, VkFormat format, VkImageUsageFlags usage, bool mipmapped = false);
void destroy_image(const AllocatedImage& img);
}

现在我们开始在 vk_engine.cpp 上编写函数

AllocatedImage VulkanEngine::create_image(VkExtent3D size, VkFormat format, VkImageUsageFlags usage, bool mipmapped)
{
	AllocatedImage newImage;
	newImage.imageFormat = format;
	newImage.imageExtent = size;

	VkImageCreateInfo img_info = vkinit::image_create_info(format, usage, size);
	if (mipmapped) {
		img_info.mipLevels = static_cast<uint32_t>(std::floor(std::log2(std::max(size.width, size.height)))) + 1;
	}

	// always allocate images on dedicated GPU memory
	VmaAllocationCreateInfo allocinfo = {};
	allocinfo.usage = VMA_MEMORY_USAGE_GPU_ONLY;
	allocinfo.requiredFlags = VkMemoryPropertyFlags(VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT);

	// allocate and create the image
	VK_CHECK(vmaCreateImage(_allocator, &img_info, &allocinfo, &newImage.image, &newImage.allocation, nullptr));

	// if the format is a depth format, we will need to have it use the correct
	// aspect flag
	VkImageAspectFlags aspectFlag = VK_IMAGE_ASPECT_COLOR_BIT;
	if (format == VK_FORMAT_D32_SFLOAT) {
		aspectFlag = VK_IMAGE_ASPECT_DEPTH_BIT;
	}

	// build a image-view for the image
	VkImageViewCreateInfo view_info = vkinit::imageview_create_info(format, newImage.image, aspectFlag);
	view_info.subresourceRange.levelCount = img_info.mipLevels;

	VK_CHECK(vkCreateImageView(_device, &view_info, nullptr, &newImage.imageView));

	return newImage;
}

这与我们之前创建绘制图像时所做的相同,只是复制到它自己的函数中。我们首先将大小和格式存储为 AllocatedImage 的一部分,然后我们使用大小、格式和用途创建一个 VkImageCreateInfo,然后我们使用 VMA 分配图像,最后创建图像视图。我们以前没有做的是设置纵横比标志。除非图像是 D32 浮点深度格式,否则我们将其默认为VK_IMAGE_ASPECT_COLOR_BIT

要写入图像数据,它的工作方式与我们在上一章中使用缓冲区所做的非常相似。我们需要创建一个临时暂存缓冲区,将我们的像素复制到其中,然后执行一个立即提交,在其中我们调用 VkCmdCopyBufferToImage。让我们也编写该函数。我们将把它作为同一个 create_image 函数的重载版本来做,但是为像素采用 void* 数据参数。我们将在这里硬编码我们的纹理为 RGBA 8 位格式,因为这是大多数图像文件所处的格式。

AllocatedImage VulkanEngine::create_image(void* data, VkExtent3D size, VkFormat format, VkImageUsageFlags usage, bool mipmapped)
{
	size_t data_size = size.depth * size.width * size.height * 4;
	AllocatedBuffer uploadbuffer = create_buffer(data_size, VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);

	memcpy(uploadbuffer.info.pMappedData, data, data_size);

	AllocatedImage new_image = create_image(size, format, usage | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT, mipmapped);

	immediate_submit([&](VkCommandBuffer cmd) {
		vkutil::transition_image(cmd, new_image.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL);

		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 = size;

		// copy the buffer into the image
		vkCmdCopyBufferToImage(cmd, uploadbuffer.buffer, new_image.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1,
			&copyRegion);

		vkutil::transition_image(cmd, new_image.image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
			VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
		});

	destroy_buffer(uploadbuffer);

	return new_image;
}

我们首先在 CPU_TO_GPU 内存类型上分配一个具有足够像素数据空间的暂存缓冲区。然后我们将像素数据 memcpy 到其中。

之后,我们调用正常的 create_image 函数,但是我们添加了 VK_IMAGE_USAGE_TRANSFER_DST_BITVK_IMAGE_USAGE_TRANSFER_SRC_BIT,以便允许将数据复制到其中和从中复制数据。

一旦我们有了图像和暂存缓冲区,我们就会运行一个立即提交,将暂存缓冲区像素数据复制到图像中。

类似于我们对交换链图像所做的那样,我们首先将图像转换为 VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL。然后我们创建一个 copyRegion 结构,其中包含复制命令的参数。这将需要图像大小以及目标图像层和 mip 级别。图像层用于具有多层的纹理,最常见的例子之一是立方体贴图纹理,它将有 6 层,每个立方体贴图面一层。我们将在稍后设置反射立方体贴图时执行此操作。

对于 mip 级别,我们将数据复制到 mip 级别 0,这是顶层。图像没有任何更多的 mip 级别。目前,我们只是将 mipmapped 布尔值传递到另一个 create_image 中,但我们没有做任何其他事情。我们将在稍后处理。

对于图像销毁,让我们填写 destroy_image() 函数

void VulkanEngine::destroy_image(const AllocatedImage& img)
{
    vkDestroyImageView(_device, img.imageView, nullptr);
    vmaDestroyImage(_allocator, img.image, img.allocation);
}

我们首先销毁图像视图,然后使用 VMA 销毁图像本身。这将正确释放图像及其内存。

有了这些函数,我们可以设置一些默认纹理。我们将创建默认白色、默认黑色、默认灰色和棋盘格纹理。这样,当某些东西加载失败时,我们可以使用一些纹理。

让我们将这些测试图像添加到 VulkanEngine 类中,并添加一些我们可以与这些图像和其他图像一起使用的采样器。

	AllocatedImage _whiteImage;
	AllocatedImage _blackImage;
	AllocatedImage _greyImage;
	AllocatedImage _errorCheckerboardImage;

    VkSampler _defaultSamplerLinear;
	VkSampler _defaultSamplerNearest;

让我们去创建它们,作为 init_default_data() 函数的一部分,在我们创建矩形网格的代码之后。

	//3 default textures, white, grey, black. 1 pixel each
	uint32_t white = glm::packUnorm4x8(glm::vec4(1, 1, 1, 1));
	_whiteImage = create_image((void*)&white, VkExtent3D{ 1, 1, 1 }, VK_FORMAT_R8G8B8A8_UNORM,
		VK_IMAGE_USAGE_SAMPLED_BIT);

	uint32_t grey = glm::packUnorm4x8(glm::vec4(0.66f, 0.66f, 0.66f, 1));
	_greyImage = create_image((void*)&grey, VkExtent3D{ 1, 1, 1 }, VK_FORMAT_R8G8B8A8_UNORM,
		VK_IMAGE_USAGE_SAMPLED_BIT);

	uint32_t black = glm::packUnorm4x8(glm::vec4(0, 0, 0, 0));
	_blackImage = create_image((void*)&black, VkExtent3D{ 1, 1, 1 }, VK_FORMAT_R8G8B8A8_UNORM,
		VK_IMAGE_USAGE_SAMPLED_BIT);

	//checkerboard image
	uint32_t magenta = glm::packUnorm4x8(glm::vec4(1, 0, 1, 1));
	std::array<uint32_t, 16 *16 > pixels; //for 16x16 checkerboard texture
	for (int x = 0; x < 16; x++) {
		for (int y = 0; y < 16; y++) {
			pixels[y*16 + x] = ((x % 2) ^ (y % 2)) ? magenta : black;
		}
	}
	_errorCheckerboardImage = create_image(pixels.data(), VkExtent3D{16, 16, 1}, VK_FORMAT_R8G8B8A8_UNORM,
		VK_IMAGE_USAGE_SAMPLED_BIT);

	VkSamplerCreateInfo sampl = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO};

	sampl.magFilter = VK_FILTER_NEAREST;
	sampl.minFilter = VK_FILTER_NEAREST;

	vkCreateSampler(_device, &sampl, nullptr, &_defaultSamplerNearest);

	sampl.magFilter = VK_FILTER_LINEAR;
	sampl.minFilter = VK_FILTER_LINEAR;
	vkCreateSampler(_device, &sampl, nullptr, &_defaultSamplerLinear);

	_mainDeletionQueue.push_function([&](){
		vkDestroySampler(_device,_defaultSamplerNearest,nullptr);
		vkDestroySampler(_device,_defaultSamplerLinear,nullptr);

		destroy_image(_whiteImage);
		destroy_image(_greyImage);
		destroy_image(_blackImage);
		destroy_image(_errorCheckerboardImage);
	});

对于 3 个默认颜色图像,我们创建以该颜色作为单像素的图像。对于棋盘格,我们编写一个 16x16 的像素颜色数据数组,其中包含一些简单的数学运算,用于黑色/品红色检查图案。

在采样器上,我们将保留所有参数为默认值,除了 min/mag 过滤器,我们将它们设置为 Linear 或 Nearest。Linear 会模糊像素,而 Nearest 会给出像素化的外观。

将图像绑定到着色器

当我们进行基于计算的渲染时,我们使用 VK_DESCRIPTOR_TYPE_STORAGE_IMAGE 绑定图像,这是我们用于没有采样逻辑的读/写纹理的类型。这大致相当于绑定缓冲区,只是一个具有不同内存布局的多维缓冲区。但是当我们进行绘制时,我们想使用 GPU 中的固定硬件来访问纹理数据,这需要采样器。我们可以选择使用 VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,它将图像和采样器打包在一起以用于该图像,或者使用 2 个描述符,并将两者分离为 VK_DESCRIPTOR_TYPE_SAMPLERVK_DESCRIPTOR_TYPE_SAMPLED_IMAGE。根据 GPU 供应商的说法,分离的方法可能会更快,因为重复数据较少。但这有点难处理,所以我们现在不会这样做。相反,我们将使用组合描述符来简化我们的着色器。

我们将修改我们之前拥有的矩形绘制,使其变为在矩形中显示图像的绘制。我们需要创建一个新的片段着色器来显示图像。让我们为此创建一个新的片段着色器。我们将其称为 tex_image.frag

//glsl version 4.5
#version 450

//shader input
layout (location = 0) in vec3 inColor;
layout (location = 1) in vec2 inUV;
//output write
layout (location = 0) out vec4 outFragColor;

//texture to access
layout(set =0, binding = 0) uniform sampler2D displayTexture;

void main() 
{
	outFragColor = texture(displayTexture,inUV);
}

我们对片段着色器有 2 个输入,颜色和 UV。着色器不使用颜色,但我们想继续使用我们之前拥有的相同顶点着色器。

要采样纹理,您可以执行 texture( textureSampler, coordinates )。还有其他函数用于直接访问给定像素等操作。纹理对象声明为 uniform sampler2D

这确实改变了我们的管线布局,所以我们也需要更新它。

让我们将布局添加到 VulkanEngine 类中,因为我们将保留它。

class VulkanEngine {
VkDescriptorSetLayout _singleImageDescriptorLayout;
}

在 init_descriptors() 中,让我们与其他部分一起创建它

{
	DescriptorLayoutBuilder builder;
	builder.add_binding(0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);
	_singleImageDescriptorLayout = builder.build(_device, VK_SHADER_STAGE_FRAGMENT_BIT);
}

一个描述符集,只有一个图像-采样器描述符。我们现在可以使用它更新 init_mesh_pipeline() 函数。我们将修改开始部分,更改片段着色器并将描述符集布局连接到管线布局创建。

void VulkanEngine::init_mesh_pipeline()
{
	VkShaderModule triangleFragShader;
	if (!vkutil::load_shader_module("../../shaders/tex_image.frag.spv", _device, &triangleFragShader)) {
		fmt::print("Error when building the fragment shader \n");
	}
	else {
		fmt::print("Triangle fragment shader succesfully loaded \n");
	}

	VkShaderModule triangleVertexShader;
	if (!vkutil::load_shader_module("../../shaders/colored_triangle_mesh.vert.spv", _device, &triangleVertexShader)) {
		fmt::print("Error when building the vertex shader \n");
	}
	else {
		fmt::print("Triangle vertex shader succesfully loaded \n");
	}

	VkPushConstantRange bufferRange{};
	bufferRange.offset = 0;
	bufferRange.size = sizeof(GPUDrawPushConstants);
	bufferRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;

	VkPipelineLayoutCreateInfo pipeline_layout_info = vkinit::pipeline_layout_create_info();
	pipeline_layout_info.pPushConstantRanges = &bufferRange;
	pipeline_layout_info.pushConstantRangeCount = 1;
	pipeline_layout_info.pSetLayouts = &_singleImageDescriptorLayout;
	pipeline_layout_info.setLayoutCount = 1;
	VK_CHECK(vkCreatePipelineLayout(_device, &pipeline_layout_info, nullptr, &_meshPipelineLayout));

}

现在,在我们的绘制函数中,我们可以动态创建绑定此管线时所需的描述符集,并使用它来显示我们要绘制的纹理。

这进入 draw_geometry() 函数,更改猴子网格的绘制

	vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _meshPipeline);

	//bind a texture
	VkDescriptorSet imageSet = get_current_frame()._frameDescriptors.allocate(_device, _singleImageDescriptorLayout);
	{
		DescriptorWriter writer;
		writer.write_image(0, _errorCheckerboardImage.imageView, _defaultSamplerNearest, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER);

		writer.update_set(_device, imageSet);
	}

	vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _meshPipelineLayout, 0, 1, &imageSet, 0, nullptr);

	glm::mat4 view = glm::translate(glm::vec3{ 0,0,-5 });
	// camera projection
	glm::mat4 projection = glm::perspective(glm::radians(70.f), (float)_drawExtent.width / (float)_drawExtent.height, 10000.f, 0.1f);

	// invert the Y direction on projection matrix so that we are more similar
	// to opengl and gltf axis
	projection[1][1] *= -1;

	GPUDrawPushConstants push_constants;
	push_constants.worldMatrix = projection * view;
	push_constants.vertexBuffer = testMeshes[2]->meshBuffers.vertexBufferAddress;

	vkCmdPushConstants(cmd, _meshPipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(GPUDrawPushConstants), &push_constants);
	vkCmdBindIndexBuffer(cmd, testMeshes[2]->meshBuffers.indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT32);

	vkCmdDrawIndexed(cmd, testMeshes[2]->surfaces[0].count, 1, testMeshes[2]->surfaces[0].startIndex, 0, 0);

我们从帧描述符集分配器分配一个新的描述符集,使用着色器使用的 _singleImageDescriptorLayout。

然后我们使用描述符写入器在绑定 0 上写入单个图像描述符,这将是 _errorCheckerboardImage。我们给它最近邻采样器,这样它就不会在像素之间混合。然后我们使用写入器更新描述符集,并绑定该集。然后我们继续绘制。

结果应该是猴头现在有一个品红色图案。

chapter2

下一步: 引擎架构