初始化分配器
我们将从确保 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.h
和 vk_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 个结构体,Vertex
和 Mesh
。Vertex
将保存位置、法线(我们稍后将使用它)和颜色。它们每个都是一个 vec3
。这种顶点格式不是最优的,因为数据可以更好地打包,但我们将使用它来简化。优化的顶点格式将是稍后的话题。
我们的 Mesh 类将保存一个 std::vector
的 Vertex
用于我们的顶点数据,以及一个 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...
}
就是这样,如果运行它,您应该会看到一个绿色三角形。它的魔力在于这个三角形不是硬编码的。它甚至不必是三角形。使用此代码,您可以渲染任何您想要的网格。
绘制代码与以前几乎相同,除了我们现在执行 vkCmdBindVertexBuffers
。通过该调用,我们告诉 Vulkan 从哪里获取顶点数据,将着色器连接到我们存储三角形数据的缓冲区。
下一步:推送常量