Link

初始化分配器

我们将从确保 Vulkan 内存分配器已初始化开始,以便我们可以轻松地从中分配缓冲区。

_allocator 成员添加到 VulkanEngine 类,以使用它。

class VulkanEngine {
public:

// other code .....
VmaAllocator _allocator; //vma lib allocator
// other code....

}

init_vulkan 函数的末尾,我们将初始化分配器。

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

    //initialize the memory allocator
    VmaAllocatorCreateInfo allocatorInfo = {};
    allocatorInfo.physicalDevice = _chosenGPU;
    allocatorInfo.device = _device;
    allocatorInfo.instance = _instance;
    vmaCreateAllocator(&allocatorInfo, &_allocator);
}

这样,分配器就设置好了,我们现在可以使用它来分配缓冲区。如果您现在尝试编译项目,您会发现 VMA 缺少函数定义并给出链接器错误。为了解决这个问题,请将此添加到项目中的一个 .cpp 文件中(建议您将其添加到 vk_engine.cpp 中,但也可以是其他文件)。

#define VMA_IMPLEMENTATION
#include "vk_mem_alloc.h"

这将包含 VMA 库本身的实现。

作为分配器的最后一件事,我们将添加一个结构体来表示分配的缓冲区到 vk_types.h

vk_types.h

#include <vk_mem_alloc.h>

struct AllocatedBuffer {
    VkBuffer _buffer;
    VmaAllocation _allocation;
};

AllocatedBuffer 将保存我们分配的缓冲区,以及其分配数据。VkBuffer 是 GPU 端 Vulkan 缓冲区的句柄,VmaAllocation 保存 VMA 库使用的状态,例如缓冲区从中分配的内存及其大小。我们使用 VmaAllocation 对象来管理缓冲区分配本身。

Mesh 类

由于我们将有很多与网格相关的代码,我们将创建一些新文件,vk_mesh.hvk_mesh.cpp,我们将把与网格相关的逻辑和结构放在其中。我们将把这些文件与引擎文件的其余部分放在一起。您可以查看 Github 代码作为示例。确保将其添加到 CMake 并重新运行它,以便项目更新。

vk_mesh.h

#pragma once

#include <vk_types.h>
#include <vector>
#include <glm/vec3.hpp>

struct Vertex {

    glm::vec3 position;
    glm::vec3 normal;
    glm::vec3 color;
};

struct Mesh {
	std::vector<Vertex> _vertices;

	AllocatedBuffer _vertexBuffer;
};

vk_mesh.cpp

#include <vk_mesh.h>
//just that for now

我们正在创建 2 个结构体,VertexMeshVertex 将保存位置、法线(我们稍后将使用它)和颜色。它们每个都是一个 vec3。这种顶点格式不是最优的,因为数据可以更好地打包,但我们将使用它来简化。优化的顶点格式将是稍后的话题。

我们的 Mesh 类将保存一个 std::vectorVertex 用于我们的顶点数据,以及一个 AllocatedBuffer,我们将在其中存储该数据的 GPU 副本。

初始化三角形网格

现在我们有了 Mesh 类,我们将使其保存三角形,并将其上传到 GPU。

回到 VulkanEngine 类,我们将添加一个三角形网格成员,以及一个初始化它的函数。除了网格,我们还将存储一个不硬编码三角形的 VkPipeline。我们还将添加一个通用的 upload_mesh 函数。

vk_engine.h

//add the include for the vk_mesh header
#include <vk_mesh.h>

class VulkanEngine {
public:

	//other code....

	VkPipeline _meshPipeline;
	Mesh _triangleMesh;

private:

	//other code ....
	void load_meshes();

	void upload_mesh(Mesh& mesh);
}

确保在 init() 函数的末尾添加 load_meshes 调用

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

	//everything went fine
	_isInitialized = true;
}

让我们开始填充 load_meshes 函数。我们要做的第一件事是用三角形的顶点数据填充 _vertices 向量,然后只需使用三角形调用 upload_mesh

void VulkanEngine::load_meshes()
{
	//make the array 3 vertices long
	_triangleMesh._vertices.resize(3);

	//vertex positions
	_triangleMesh._vertices[0].position = { 1.f, 1.f, 0.0f };
	_triangleMesh._vertices[1].position = {-1.f, 1.f, 0.0f };
	_triangleMesh._vertices[2].position = { 0.f,-1.f, 0.0f };

	//vertex colors, all green
	_triangleMesh._vertices[0].color = { 0.f, 1.f, 0.0f }; //pure green
	_triangleMesh._vertices[1].color = { 0.f, 1.f, 0.0f }; //pure green
	_triangleMesh._vertices[2].color = { 0.f, 1.f, 0.0f }; //pure green

	//we don't care about the vertex normals

	upload_mesh(_triangleMesh);
}

现在是创建顶点缓冲区的时候了。我们将填充 upload_mesh 函数

void VulkanEngine::upload_mesh(Mesh& mesh)
{
	//allocate vertex buffer
	VkBufferCreateInfo bufferInfo = {};
	bufferInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO;
	//this is the total size, in bytes, of the buffer we are allocating
	bufferInfo.size = mesh._vertices.size() * sizeof(Vertex);
	//this buffer is going to be used as a Vertex Buffer
	bufferInfo.usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;


	//let the VMA library know that this data should be writeable by CPU, but also readable by GPU
	VmaAllocationCreateInfo vmaallocInfo = {};
	vmaallocInfo.usage = VMA_MEMORY_USAGE_CPU_TO_GPU;

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

	//add the destruction of triangle mesh buffer to the deletion queue
	_mainDeletionQueue.push_function([=]() {

        vmaDestroyBuffer(_allocator, mesh._vertexBuffer._buffer, mesh._vertexBuffer._allocation);
    });
}

这将使用 VMA 库分配缓冲区,并将其释放添加到销毁队列。请注意我们如何需要将 VmaAllocation 对象发送到 vmaDestroyBuffer,这就是我们将它们放在一起的原因。

当您创建缓冲区时,您可以决定在哪个内存中创建它。使用 VMA 库,这通过 VMA_MEMORY_USAGE_ 枚举来抽象。这将让 VMA 决定在哪里分配内存。VMA_MEMORY_USAGE_CPU_TO_GPU 用法对于动态数据非常有用。它可以从 CPU 写入,VMA 将尝试将分配放置在直接 GPU 可访问的内存中。

有许多可能的方式来使用给定的缓冲区(作为纹理存储、作为 uniform、作为可写数据等),因此 Vulkan 需要知道您到底要将该缓冲区用于什么。我们将严格将其用作顶点缓冲区,因此我们只需将 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT 标志放在 VkBufferCreateInfo::usage 参数中。如果您以未指定的方式使用缓冲区,则验证层会报错。

现在我们为顶点数据获得了一个内存位置,我们从 _vertices 向量复制到这个 GPU 可读的数据中。

void VulkanEngine::upload_mesh(Mesh& mesh)
{
	// other code ....

	//copy vertex data
	void* data;
	vmaMapMemory(_allocator, mesh._vertexBuffer._allocation, &data);

	memcpy(data, mesh._vertices.data(), mesh._vertices.size() * sizeof(Vertex));

	vmaUnmapMemory(_allocator, mesh._vertexBuffer._allocation);
}

要将数据推送到 VkBuffer 中,我们需要首先映射它。映射缓冲区将为我们提供一个指针(这里的 data),然后我们可以写入其中。当我们完成写入后,我们取消映射数据。可以保持指针映射而不立即取消映射,但这是一种主要用于流式传输数据的高级技术,我们现在不需要。映射然后取消映射指针让驱动程序知道写入已完成,并且会更安全。

要复制数据,我们直接使用 memcpy。请注意,没有必要使用 memcpy,但在许多实现中,memcpy 将是复制内存块的最快方法。

我们的 upload_mesh 函数已完成(目前),我们正在上传三角形。当您尝试运行应用程序时,应该没有验证错误。

顶点输入布局

三角形现在位于 GPU 可访问的内存中,因此我们现在必须更改管线以渲染它。我们首先创建一个结构体来保存输入布局数据,以及 Vertex 中的一个静态函数,该函数创建一个描述以匹配 Vertex 中的格式。

vk_mesh.h

struct VertexInputDescription {

	std::vector<VkVertexInputBindingDescription> bindings;
	std::vector<VkVertexInputAttributeDescription> attributes;

	VkPipelineVertexInputStateCreateFlags flags = 0;
};


struct Vertex {

	glm::vec3 position;
	glm::vec3 normal;
	glm::vec3 color;

	static VertexInputDescription get_vertex_description();
};

让我们用正确的设置填充 get_vertex_description 函数。vk_mesh.cpp


VertexInputDescription Vertex::get_vertex_description()
{
	VertexInputDescription description;

	//we will have just 1 vertex buffer binding, with a per-vertex rate
	VkVertexInputBindingDescription mainBinding = {};
	mainBinding.binding = 0;
	mainBinding.stride = sizeof(Vertex);
	mainBinding.inputRate = VK_VERTEX_INPUT_RATE_VERTEX;

	description.bindings.push_back(mainBinding);

	//Position will be stored at Location 0
	VkVertexInputAttributeDescription positionAttribute = {};
	positionAttribute.binding = 0;
	positionAttribute.location = 0;
	positionAttribute.format = VK_FORMAT_R32G32B32_SFLOAT;
	positionAttribute.offset = offsetof(Vertex, position);

	 //Normal will be stored at Location 1
	VkVertexInputAttributeDescription normalAttribute = {};
	normalAttribute.binding = 0;
	normalAttribute.location = 1;
	normalAttribute.format = VK_FORMAT_R32G32B32_SFLOAT;
	normalAttribute.offset = offsetof(Vertex, normal);

	//Color will be stored at Location 2
	VkVertexInputAttributeDescription colorAttribute = {};
	colorAttribute.binding = 0;
	colorAttribute.location = 2;
	colorAttribute.format = VK_FORMAT_R32G32B32_SFLOAT;
	colorAttribute.offset = offsetof(Vertex, color);

	description.attributes.push_back(positionAttribute);
	description.attributes.push_back(normalAttribute);
	description.attributes.push_back(colorAttribute);
	return description;
}

VkVertexInputBindingDescription 定义了充当输入的顶点缓冲区。在这种情况下,我们仅使用 1 个顶点缓冲区,因此我们只需要一个绑定。步幅为 sizeof(Vertex),因为我们的顶点数据紧密打包,每个顶点占用 sizeof(Vertex) 大小。

然后我们创建 3 个 VkVertexInputAttributeDescription,每个顶点属性一个。在每个属性上,我们将格式设置为 VK_FORMAT_R32G32B32_SFLOAT,它直接映射到 glm::vec3 是什么(三个 32 位 float 分量),并使用 Vertex 结构中成员的偏移量。

这样,我们现在直接将我们的 Vertex 结构映射到 Vulkan 在管线顶点输入中期望的内容。

新顶点着色器

现在让我们创建一个新的着色器,tri_mesh.vert,它将使用这些顶点输入。此顶点着色器将与 colored_triangle.frag 片段着色器一起使用。确保刷新 CMake,以便它找到新的着色器并对其进行编译。

tri_mesh.vert

#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;

void main()
{
	gl_Position = vec4(vPosition, 1.0f);
	outColor = vColor;
}

我们不再需要使用 vertexID 来做任何事情,我们可以直接将 vPosition 发送到 gl_Position。通过顶点 ID 获取数据将由驱动程序在编译管线时自动完成。所有 3 个属性的工作方式相同,根据我们刚刚编写的 VkVertexInputAttributeDescription

将所有内容放在一起

我们现在已经上传了缓冲区,编写了着色器,并填充了输入描述。现在是编译 _meshPipeline 的时候了,并使用它来渲染我们的新三角形,它应该是绿色的。

转到 VulkanEngine 的 init_pipelines 函数,因为我们将在其末尾创建管线。

void VulkanEngine::init_pipelines()
{
	//other code


	//build the mesh pipeline

	VertexInputDescription vertexDescription = Vertex::get_vertex_description();

	//connect the pipeline builder vertex input info to the one we get from Vertex
	pipelineBuilder._vertexInputInfo.pVertexAttributeDescriptions = vertexDescription.attributes.data();
	pipelineBuilder._vertexInputInfo.vertexAttributeDescriptionCount = vertexDescription.attributes.size();

	pipelineBuilder._vertexInputInfo.pVertexBindingDescriptions = vertexDescription.bindings.data();
	pipelineBuilder._vertexInputInfo.vertexBindingDescriptionCount = vertexDescription.bindings.size();

	//clear the shader stages for the builder
	pipelineBuilder._shaderStages.clear();

	//compile mesh vertex shader


	VkShaderModule meshVertShader;
	if (!load_shader_module("../../shaders/tri_mesh.vert.spv", &meshVertShader))
	{
		std::cout << "Error when building the triangle vertex shader module" << std::endl;
	}
	else {
		std::cout << "Red Triangle vertex shader successfully loaded" << std::endl;
	}

	//add the other shaders
	pipelineBuilder._shaderStages.push_back(
		vkinit::pipeline_shader_stage_create_info(VK_SHADER_STAGE_VERTEX_BIT, meshVertShader));

	//make sure that triangleFragShader is holding the compiled colored_triangle.frag
	pipelineBuilder._shaderStages.push_back(
		vkinit::pipeline_shader_stage_create_info(VK_SHADER_STAGE_FRAGMENT_BIT, triangleFragShader));

	//build the mesh triangle pipeline
	_meshPipeline = pipelineBuilder.build_pipeline(_device, _renderPass);

	//deleting all of the vulkan shaders
	vkDestroyShaderModule(_device, meshVertShader, nullptr);
	vkDestroyShaderModule(_device, redTriangleVertShader, nullptr);
	vkDestroyShaderModule(_device, redTriangleFragShader, nullptr);
	vkDestroyShaderModule(_device, triangleFragShader, nullptr);
	vkDestroyShaderModule(_device, triangleVertexShader, nullptr);

	//adding the pipelines to the deletion queue
	_mainDeletionQueue.push_function([=]() {
		vkDestroyPipeline(_device, _redTrianglePipeline, nullptr);
		vkDestroyPipeline(_device, _trianglePipeline, nullptr);
		vkDestroyPipeline(_device, _meshPipeline, nullptr);

		vkDestroyPipelineLayout(_device, _trianglePipelineLayout, nullptr);
	});
}

这里没有太多内容,除了将顶点输入信息连接到管线构建器。有了它并添加 tri_mesh.vert 顶点着色器,这就是我们所需要的。我们还确保在函数末尾正确删除每个着色器模块。

现在我们持有一个 _meshPipeline,它知道如何渲染彩色网格。让我们替换 draw() 函数的内部循环,以使用新的管线并绘制网格。

VulkanEngine::draw()
{
	//other code ....
	vkCmdBeginRenderPass(cmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE);

	vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _meshPipeline);

	//bind the mesh vertex buffer with offset 0
	VkDeviceSize offset = 0;
	vkCmdBindVertexBuffers(cmd, 0, 1, &_triangleMesh._vertexBuffer._buffer, &offset);

	//we can now draw the mesh
	vkCmdDraw(cmd, _triangleMesh._vertices.size(), 1, 0, 0);

	//finalize the render pass
	vkCmdEndRenderPass(cmd);

	// other code...
}

triangle

就是这样,如果运行它,您应该会看到一个绿色三角形。它的魔力在于这个三角形不是硬编码的。它甚至不必是三角形。使用此代码,您可以渲染任何您想要的网格。

绘制代码与以前几乎相同,除了我们现在执行 vkCmdBindVertexBuffers。通过该调用,我们告诉 Vulkan 从哪里获取顶点数据,将着色器连接到我们存储三角形数据的缓冲区。

下一步:推送常量