Link

既然已经解释了描述符集,让我们看看在一个最小的示例中如何在实践中使用它们。我们将修改代码库和着色器,以便不再通过推送常量发送对象的最终变换矩阵,而是在着色器上读取相机矩阵,并将相机矩阵与对象矩阵相乘,乘法运算在着色器中完成。为了实现这一点,我们需要为我们的相机矩阵创建一个 uniform 缓冲区,并使用单个描述符集将其暴露给着色器。

设置相机缓冲区

我们将为每个帧创建一个相机缓冲区。这样做是为了我们可以正确地重叠数据,并在 GPU 渲染最后一帧时修改相机矩阵。

鉴于我们开始创建大量的缓冲区,我们将首先将缓冲区创建抽象成一个函数。

将函数声明添加到 VulkanEngine 类 vk_engine.cpp 中

AllocatedBuffer VulkanEngine::create_buffer(size_t allocSize, VkBufferUsageFlags usage, VmaMemoryUsage memoryUsage)
{
	//allocate vertex buffer
	VkBufferCreateInfo bufferInfo = {};
	bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
	bufferInfo.pNext = nullptr;

	bufferInfo.size = allocSize;
	bufferInfo.usage = usage;


	VmaAllocationCreateInfo vmaallocInfo = {};
	vmaallocInfo.usage = memoryUsage;

	AllocatedBuffer newBuffer;

	//allocate the buffer
	VK_CHECK(vmaCreateBuffer(_allocator, &bufferInfo, &vmaallocInfo,
		&newBuffer._buffer,
		&newBuffer._allocation,
		nullptr));

	return newBuffer;
}

在该函数中,我们将仅请求缓冲区大小、缓冲区用途和内存用途。这是基本缓冲区创建所需的一切。这与我们用于顶点缓冲区的代码类似。

现在,我们将添加一个变量来保存相机缓冲区到我们的 FrameData 结构中,并为相机数据创建一个结构体。


struct GPUCameraData{
	glm::mat4 view;
	glm::mat4 proj;
	glm::mat4 viewproj;
};

struct FrameData {
	// other code ...
	//buffer that holds a single GPUCameraData to use when rendering
	AllocatedBuffer cameraBuffer;

	VkDescriptorSet globalDescriptor;
};

GPUCameraData 结构体仅保存我们将需要的一些矩阵。视图矩阵(相机位置/变换)、投影矩阵(用于透视)和 ViewProj,它只是将它们两个相乘,以避免在着色器中相乘。

在 FrameData 上,我们为其添加了 AllocatedBuffer,但我们也添加了一个 VkDescriptorSet,我们将缓存它以保存全局描述符。我们将向其中添加一些比相机 uniform 缓冲区更多的东西。

我们将添加另一个初始化函数 init_descriptors() 到 VulkanEngine 类。也将其添加到主 init 函数中,但在 init_pipelines() 调用之前。我们在那里初始化的一些描述符内容在创建管线时将需要。

class VulkanEngine {
	//other code....
	AllocatedBuffer create_buffer(size_t allocSize, VkBufferUsageFlags usage, VmaMemoryUsage memoryUsage);
	void init_descriptors();
}
void VulkanEngine::init()
{
	// other code ....

	init_sync_structures();

	init_descriptors();

	init_pipelines();

	//other code ....
}

现在函数和数据已添加,我们需要创建这些相机缓冲区。

void VulkanEngine::init_descriptors()
{
	for (int i = 0; i < FRAME_OVERLAP; i++)
	{
		_frames[i].cameraBuffer = create_buffer(sizeof(GPUCameraData), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);
	}

	// add buffers to deletion queues
	for (int i = 0; i < FRAME_OVERLAP; i++)
	{
		_mainDeletionQueue.push_function([&]() {
			vmaDestroyBuffer(_allocator, _frames[i].cameraBuffer._buffer, _frames[i].cameraBuffer._allocation);
		});
	}
}

为了创建缓冲区,我们将使用 Uniform Buffer 用途和 CPU_TO_GPU 内存类型。Uniform 缓冲区最适合这种小型、只读的着色器数据。它们具有大小限制,但在着色器中访问速度非常快。

尝试运行此代码,看看验证层是否报错。它们不应该报错。

描述符集:着色器

我们将开始处理着色器数据本身。第一件事是修改我们使用的着色器,以在那里使用矩阵。

#version 450
layout (location = 0) in vec3 vPosition;
layout (location = 1) in vec3 vNormal;
layout (location = 2) in vec3 vColor;

layout (location = 0) out vec3 outColor;

layout(set = 0, binding = 0) uniform  CameraBuffer{
	mat4 view;
	mat4 proj;
	mat4 viewproj;
} cameraData;

//push constants block
layout( push_constant ) uniform constants
{
 vec4 data;
 mat4 render_matrix;
} PushConstants;

void main()
{
	mat4 transformMatrix = (cameraData.viewproj * PushConstants.render_matrix);
	gl_Position = transformMatrix * vec4(vPosition, 1.0f);
	outColor = vColor;
}

新的块是 CameraBuffer uniform 声明。在其中,你可以看到它遵循与推送常量块相同的语法,但具有不同的 layout()。通过具有 set = 0binding = 0,我们声明 CameraBuffer uniform 将从在槽 0 绑定的描述符集中获取,并且它是该描述符集中的绑定 0。

在顶点着色器的核心中,我们将推送常量中的渲染矩阵与 CameraBuffer 上的 viewproj 矩阵相乘。这将获得最终变换矩阵,然后我们可以将顶点位置乘以它。

现在让我们在 cpp 端进行设置。我们需要的第一件事是创建描述符集布局。

描述符集布局

向 vulkan 引擎添加一个新的成员变量。我们将使用它来存储全局数据的描述符布局。 также 添加一个成员用于我们稍后需要的描述符池

class VulkanEngine {
VkDescriptorSetLayout _globalSetLayout;
VkDescriptorPool _descriptorPool;
}

VkDescriptorSetLayout 保存有关描述符集形状的信息。在这种情况下,我们的描述符集将是一个在绑定 0 处保存单个 uniform 缓冲区引用的集合。

void VulkanEngine::init_descriptors()
{

	//information about the binding.
	VkDescriptorSetLayoutBinding camBufferBinding = {};
	camBufferBinding.binding = 0;
	camBufferBinding.descriptorCount = 1;
	// it's a uniform buffer binding
	camBufferBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;

	// we use it from the vertex shader
	camBufferBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;


	VkDescriptorSetLayoutCreateInfo setinfo = {};
	setinfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
	setinfo.pNext = nullptr;

	//we are going to have 1 binding
	setinfo.bindingCount = 1;
	//no flags
	setinfo.flags = 0;
	//point to the camera buffer binding
	setinfo.pBindings = &camBufferBinding;

	vkCreateDescriptorSetLayout(_device, &setinfo, nullptr, &_globalSetLayout);

	// other code ....

	// add descriptor set layout to deletion queues
	_mainDeletionQueue.push_function([&]() {
		vkDestroyDescriptorSetLayout(_device, _globalSetLayout, nullptr);
	}
}

要创建描述符集布局,我们需要另一个 CreateInfo 结构体。create-info 将指向 VkDescriptorSetLayoutBinding 结构体的数组。这些结构体中的每一个都将包含有关描述符本身的信息。在这种情况下,我们只有一个绑定,即绑定 0,它是一个 Uniform Buffer。

我们现在有了为我们的描述符创建的描述符集布局,因此我们需要将其连接到管线创建。当您创建管线时,您还需要让管线知道哪些描述符将被绑定到它。

回到 init_pipelines()。我们需要通过将描述符布局连接到它来修改 VkPipelineLayout 的创建。


//push-constant setup
mesh_pipeline_layout_info.pPushConstantRanges = &push_constant;
mesh_pipeline_layout_info.pushConstantRangeCount = 1;

//hook the global set layout
mesh_pipeline_layout_info.setLayoutCount = 1;
mesh_pipeline_layout_info.pSetLayouts = &_globalSetLayout;

VkPipelineLayout meshPipLayout;
VK_CHECK(vkCreatePipelineLayout(_device, &mesh_pipeline_layout_info, nullptr, &meshPipLayout));

现在我们的管线构建器将管线布局连接到所有内容,这将允许管线在我们绑定描述符集后访问它们。

管线设置完成,所以现在我们必须分配一个描述符集,并在渲染时绑定它。

分配描述符集

回到 init_descriptors(),我们首先需要创建一个 VkDescriptorPool 以从中分配描述符。

void VulkanEngine::init_descriptors()
{

	//create a descriptor pool that will hold 10 uniform buffers
	std::vector<VkDescriptorPoolSize> sizes =
	{
		{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 10 }
	};

	VkDescriptorPoolCreateInfo pool_info = {};
	pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
	pool_info.flags = 0;
	pool_info.maxSets = 10;
	pool_info.poolSizeCount = (uint32_t)sizes.size();
	pool_info.pPoolSizes = sizes.data();

	vkCreateDescriptorPool(_device, &pool_info, nullptr, &_descriptorPool);

	// other code ....

	// add descriptor set layout to deletion queues
	_mainDeletionQueue.push_function([&]() {
		vkDestroyDescriptorSetLayout(_device, _globalSetLayout, nullptr);
		vkDestroyDescriptorPool(_device, _descriptorPool, nullptr);
	}
}

在这种情况下,我们确切地知道我们需要从池中分配什么,即指向 uniform 缓冲区的描述符集。在创建描述符池时,您需要指定您将需要多少每种类型的描述符,以及从其中分配的最大集合数。现在,我们将保留 10 个 uniform 缓冲区指针/句柄,以及从池中分配的最大 10 个描述符集。

我们现在可以从中分配描述符。为此,继续在 init_descriptors() 函数中的 FRAME_OVERLAP 循环中。

for (int i = 0; i < FRAME_OVERLAP; i++)
	{
		_frames[i].cameraBuffer = create_buffer(sizeof(GPUCameraData), VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);

		//allocate one descriptor set for each frame
		VkDescriptorSetAllocateInfo allocInfo ={};
		allocInfo.pNext = nullptr;
		allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
		//using the pool we just set
		allocInfo.descriptorPool = _descriptorPool;
		//only 1 descriptor
		allocInfo.descriptorSetCount = 1;
		//using the global data layout
		allocInfo.pSetLayouts = &_globalSetLayout;

		vkAllocateDescriptorSets(_device, &allocInfo, &_frames[i].globalDescriptor);
	}

有了这个,我们现在在我们的帧结构中存储了一个描述符。但是这个描述符尚未指向任何缓冲区,因此我们需要使其指向我们的相机缓冲区。


for (int i = 0; i < FRAME_OVERLAP; i++)
	{
		// allocation code ...

		//information about the buffer we want to point at in the descriptor
		VkDescriptorBufferInfo binfo;
		//it will be the camera buffer
		binfo.buffer = _frames[i].cameraBuffer._buffer;
		//at 0 offset
		binfo.offset = 0;
		//of the size of a camera data struct
		binfo.range = sizeof(GPUCameraData);

		VkWriteDescriptorSet setWrite = {};
		setWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
		setWrite.pNext = nullptr;

		//we are going to write into binding number 0
		setWrite.dstBinding = 0;
		//of the global descriptor
		setWrite.dstSet = _frames[i].globalDescriptor;

		setWrite.descriptorCount = 1;
		//and the type is uniform buffer
		setWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
		setWrite.pBufferInfo = &binfo;


		vkUpdateDescriptorSets(_device, 1, &setWrite, 0, nullptr);
	}

我们需要使用我们想要在描述符集中拥有的缓冲区数据填充 VkDescriptorBufferInfo。因为我们已经定义了我们的相机缓冲区在绑定 0 上,那么我们需要在此处设置它,并具有足够的大小来容纳结构体。

现在我们有了一个填充的描述符集,所以我们可以在渲染时使用它。在 draw_objects() 函数中,我们将首先使用当前相机矩阵写入相机缓冲区。这是我们之前用于推送常量的代码,但我们现在填充一个 GPUCameraData 结构体,然后将其复制到缓冲区中。如果您已经实现了移动相机,则需要修改此代码。


void VulkanEngine::draw_objects(VkCommandBuffer cmd,RenderObject* first, int count)
{

	//camera view
	glm::vec3 camPos = { 0.f,-6.f,-10.f };

	glm::mat4 view = glm::translate(glm::mat4(1.f), camPos);
	//camera projection
	glm::mat4 projection = glm::perspective(glm::radians(70.f), 1700.f / 900.f, 0.1f, 200.0f);
	projection[1][1] *= -1;

	//fill a GPU camera data struct
	GPUCameraData camData;
	camData.proj = projection;
	camData.view = view;
	camData.viewproj = projection * view;

	//and copy it to the buffer
	void* data;
	vmaMapMemory(_allocator, get_current_frame().cameraBuffer._allocation, &data);

	memcpy(data, &camData, sizeof(GPUCameraData));

	vmaUnmapMemory(_allocator, get_current_frame().cameraBuffer._allocation);
}

我们填充结构体,然后使用与处理顶点缓冲区时相同的模式将其复制到缓冲区。首先,您将缓冲区映射到一个 void 指针,然后将数据 memcpy 到其中,然后取消映射缓冲区。

缓冲区现在保存了正确的相机数据,所以现在我们可以绑定它。

//only bind the pipeline if it doesn't match with the already bound one
if (object.material != lastMaterial) {

	vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, object.material->pipeline);
	lastMaterial = object.material;
	//bind the descriptor set when changing pipeline
	vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, object.material->pipelineLayout, 0, 1, &get_current_frame().globalDescriptor, 0, nullptr);
}

我们将在每次切换管线时绑定该集合。现在这不是绝对必要的,因为我们所有的管线都是相同的,但这会更容易。

最后一件事是修改推送常量代码,使其不在那里乘以矩阵,而只是推送常量模型矩阵。

MeshPushConstants constants;
constants.render_matrix = object.transformMatrix;

//upload the mesh to the GPU via push constants
vkCmdPushConstants(cmd, object.material->pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(MeshPushConstants), &constants);

如果您现在运行代码,一切都应该正常工作。但是我们现在有一种方法可以让着色器从缓冲区读取一些数据,而不必推送常量着色器的所有数据。

下一步:动态描述符集