统一缓冲区非常适合小型、只读数据。但是,如果您想要着色器中大小未知的数据呢?或者可以写入的数据呢?您可以使用存储缓冲区来实现。存储缓冲区通常比统一缓冲区稍慢,但它们可以大得多得多。如果您想将整个场景塞进一个缓冲区中,则必须使用它们。请务必进行性能分析以了解性能。
使用存储缓冲区,您可以在着色器中使用任何您想要的数据的无大小数组。它们的一个常见用途是存储场景中所有对象的数据。
我们将使用它们来移除对对象矩阵使用推送常量的做法,这将使我们能够在帧开始时批量上传矩阵,然后我们不再需要每次绘制都进行单独的推送常量调用。这也意味着我们将把所有对象矩阵保存在一个数组中,该数组可用于计算着色器中的有趣事物。
创建着色器存储缓冲区
我们将继续在 init_descriptors()
函数中进行操作,因为它是在其中初始化着色器参数的所有缓冲区的地方。在那里,我们将为每帧初始化一个大的存储缓冲区,以保存对象的数据。这是因为我们希望对象仍然是动态的。如果我们有完全静态的对象,我们就不需要每帧一个缓冲区,一个总的缓冲区就足够了。
struct FrameData {
AllocatedBuffer objectBuffer;
};
struct GPUObjectData{
glm::mat4 modelMatrix;
};
void VulkanEngine::init_descriptors()
{
// other code ...
for (int i = 0; i < FRAME_OVERLAP; i++)
{
const int MAX_OBJECTS = 10000;
_frames[i].objectBuffer = create_buffer(sizeof(GPUObjectData) * MAX_OBJECTS, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, VMA_MEMORY_USAGE_CPU_TO_GPU);
//other code ....
}
// add storage buffers to deletion queues
for (int i = 0; i < FRAME_OVERLAP; i++)
{
vmaDestroyBuffer(_allocator, _frames[i].objectBuffer._buffer, _frames[i].objectBuffer._allocation);
//other code ....
}
}
着色器存储缓冲区的创建方式与统一缓冲区相同。它们的工作方式也基本相同,只是具有不同的属性,例如增加的最大大小,以及在着色器中可写。我们将为每帧保留一个包含 10000 个 ObjectData 的数组。这意味着我们可以容纳多达 10000 个对象矩阵,每帧渲染 10000 个对象。这个数字很小,但目前这不是问题。Unreal Engine 在引擎加载更多对象时会根据需要增长其对象缓冲区,但我们没有任何可增长的缓冲区抽象,因此我们预先保留。虽然这里的大小是 10000,但您可以将其增加到您想要的任何大小。存储缓冲区的最大大小非常大,在大多数 GPU 中,它们可以与 VRAM 可以容纳的大小一样大,因此如果您愿意,可以使用 1 亿个矩阵来执行此操作。
我们现在需要将其添加到描述符集中。我们一直将所有内容添加到描述符集编号 0 中,但对于此操作,我们将使用描述符集编号 1。这意味着我们需要另一个描述符集布局,并将其挂钩到管线创建中。
新的描述符集
struct FrameData {
AllocatedBuffer objectBuffer;
VkDescriptorSet objectDescriptor;
};
class VulkanEngine {
VkDescriptorSetLayout _globalSetLayout;
VkDescriptorSetLayout _objectSetLayout;
}
我们将采用与相机缓冲区类似的方法,即我们将有一个描述符指向一个缓冲区。因为这是一个新的描述符集,所以我们还需要存储其布局以挂钩到管线。
回到 init_descriptors()
,我们需要在描述符池上为其保留空间。
std::vector<VkDescriptorPoolSize> sizes =
{
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 10 },
{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 10 },
{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 10 }
};
在下面一点,我们将初始化集布局,它将仅具有 1 个绑定用于大缓冲区。
VkDescriptorSetLayoutBinding objectBind = vkinit::descriptorset_layout_binding(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, VK_SHADER_STAGE_VERTEX_BIT, 0);
VkDescriptorSetLayoutCreateInfo set2info = {};
set2info.bindingCount = 1;
set2info.flags = 0;
set2info.pNext = nullptr;
set2info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
set2info.pBindings = &objectBind;
vkCreateDescriptorSetLayout(_device, &set2info, nullptr, &_objectSetLayout);
与另一个集相同,我们为新集创建一个布局,该布局将指向 1 个存储缓冲区。
我们不应忘记将此新布局添加到删除队列中
_mainDeletionQueue.push_function([&]() {
// other code ....
vkDestroyDescriptorSetLayout(_device, _objectSetLayout, nullptr);
}
现在我们需要创建描述符集以指向缓冲区。
for (int i = 0; i < FRAME_OVERLAP; i++)
{
//allocation for the other descriptor and other code....
//allocate the descriptor set that will point to object buffer
VkDescriptorSetAllocateInfo objectSetAlloc = {};
objectSetAlloc.pNext = nullptr;
objectSetAlloc.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO;
objectSetAlloc.descriptorPool = _descriptorPool;
objectSetAlloc.descriptorSetCount = 1;
objectSetAlloc.pSetLayouts = &_objectSetLayout;
vkAllocateDescriptorSets(_device, &objectSetAlloc, &_frames[i].objectDescriptor);
VkDescriptorBufferInfo cameraInfo;
cameraInfo.buffer = _frames[i].cameraBuffer._buffer;
cameraInfo.offset = 0;
cameraInfo.range = sizeof(GPUCameraData);
VkDescriptorBufferInfo sceneInfo;
sceneInfo.buffer = _sceneParameterBuffer._buffer;
sceneInfo.offset = 0;
sceneInfo.range = sizeof(GPUSceneData);
VkDescriptorBufferInfo objectBufferInfo;
objectBufferInfo.buffer = _frames[i].objectBuffer._buffer;
objectBufferInfo.offset = 0;
objectBufferInfo.range = sizeof(GPUObjectData) * MAX_OBJECTS;
VkWriteDescriptorSet cameraWrite = vkinit::write_descriptor_buffer(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, _frames[i].globalDescriptor,&cameraInfo,0);
VkWriteDescriptorSet sceneWrite = vkinit::write_descriptor_buffer(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, _frames[i].globalDescriptor, &sceneInfo, 1);
VkWriteDescriptorSet objectWrite = vkinit::write_descriptor_buffer(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, _frames[i].objectDescriptor, &objectBufferInfo, 0);
VkWriteDescriptorSet setWrites[] = { cameraWrite,sceneWrite,objectWrite };
vkUpdateDescriptorSets(_device, 3, setWrites, 0, nullptr);
}
我们需要另一个 DescriptorBufferInfo 和另一个 WriteDescriptorSet。请注意,在这里,我们正在使用 1 个 vkUpdateDescriptorSets()
调用来更新 2 个不同的描述符集。这样做是完全有效的。现在缓冲区已初始化并且具有指向它的描述符,我们需要将其添加到着色器中。
我们将修改 tri_mesh.vert
着色器,以从 SSBO 而不是从推送常量读取对象数据。我们将仍然保留推送常量,但它不会被使用。
#version 460
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;
struct ObjectData{
mat4 model;
};
//all object matrices
layout(std140,set = 1, binding = 0) readonly buffer ObjectBuffer{
ObjectData objects[];
} objectBuffer;
//push constants block
layout( push_constant ) uniform constants
{
vec4 data;
mat4 render_matrix;
} PushConstants;
void main()
{
mat4 modelMatrix = objectBuffer.objects[gl_BaseInstance].model;
mat4 transformMatrix = (cameraData.viewproj * modelMatrix);
gl_Position = transformMatrix * vec4(vPosition, 1.0f);
outColor = vColor;
}
我们将 GLSL 版本更改为 460,因为我们希望能够使用 gl_BaseInstance
来索引到变换数组中。
并在 init_vulkan()
中启用 shader draw parameters
功能
void VulkanEngine::init_vulkan(){
// initialize vulkan instance and surface ...
// create the final vulkan device
vkb::DeviceBuilder deviceBuilder{physicalDevice};
VkPhysicalDeviceShaderDrawParametersFeatures shader_draw_parameters_features = {};
shader_draw_parameters_features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SHADER_DRAW_PARAMETERS_FEATURES;
shader_draw_parameters_features.pNext = nullptr;
shader_draw_parameters_features.shaderDrawParameters = VK_TRUE;
vkb::Device vkbDevice = deviceBuilder.add_pNext(&shader_draw_parameters_features).build().value();
// other code ...
}
请注意我们声明 ObjectBuffer 的方式
layout(std140,set = 1, binding = 0) readonly buffer ObjectBuffer{
ObjectData objects[];
} objectBuffer;
我们需要 std140 布局描述,以使数组与 cpp 中数组的工作方式相匹配。该 std140 强制执行有关内存如何布局及其对齐方式的一些规则。该集现在是 1,绑定是 0,表示它是一个新的描述符集槽。
在声明它时,我们还使用 readonly buffer
而不是 uniform
。着色器存储缓冲区可以读取或写入,因此我们需要让 Vulkan 知道。它们也使用 buffer
而不是 uniform
定义。
内部的数组也是无大小的。您只能在存储缓冲区中使用无大小的数组。这将使着色器可以缩放到我们拥有的任何缓冲区大小。
另一件事是我们访问正确对象矩阵的方式。我们不再使用推送常量,而是这样做
mat4 modelMatrix = objectBuffer.objects[gl_BaseInstance].model;
我们正在使用 gl_BaseInstance
来访问对象缓冲区。这是因为 Vulkan 在其正常绘制调用中的工作方式。Vulkan 中的所有绘制命令都请求“第一个实例”和“实例计数”。我们没有进行实例化渲染,因此实例计数始终为 1。但是我们仍然可以更改“第一个实例”参数,并通过这种方式获得 gl_BaseInstance
,作为一个我们可以用于着色器中任何用途的整数。这为我们提供了一种简单的方法,无需设置推送常量或描述符即可向着色器发送单个整数。
我们现在需要将描述符布局挂钩到管线。
在 init_pipelines()
上,我们在创建管线布局时将其添加到描述符列表中
VkDescriptorSetLayout setLayouts[] = { _globalSetLayout, _objectSetLayout };
mesh_pipeline_layout_info.setLayoutCount = 2;
mesh_pipeline_layout_info.pSetLayouts = setLayouts;
我们现在已经设置好了管线,所以最后一件事是写入缓冲区。
写入着色器存储缓冲区
在 draw_objects()
上,我们将通过将渲染对象的渲染矩阵复制到缓冲区中来写入缓冲区。这在渲染循环之前进行,与其他的内存写入操作一起。
void* objectData;
vmaMapMemory(_allocator, get_current_frame().objectBuffer._allocation, &objectData);
GPUObjectData* objectSSBO = (GPUObjectData*)objectData;
for (int i = 0; i < count; i++)
{
RenderObject& object = first[i];
objectSSBO[i].modelMatrix = object.transformMatrix;
}
vmaUnmapMemory(_allocator, get_current_frame().objectBuffer._allocation);
我们没有在这里使用 memcpy
,而是使用了不同的技巧。可以从映射缓冲区到另一种类型的 void*
进行类型转换,并正常写入。这将完全正常工作,并且可以更轻松地将复杂类型写入缓冲区。
缓冲区现在已填充,因此我们现在需要在绘制命令中绑定描述符集并使用 firstIndex 参数来访问着色器中的对象数据。
if (object.material != lastMaterial) {
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, object.material->pipeline);
lastMaterial = object.material;
//camera data descriptor
uint32_t uniform_offset = pad_uniform_buffer_size(sizeof(GPUSceneData)) * frameIndex;
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, object.material->pipelineLayout, 0, 1, &get_current_frame().globalDescriptor, 1, &uniform_offset);
//object data descriptor
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, object.material->pipelineLayout, 1, 1, &get_current_frame().objectDescriptor, 0, nullptr);
}
//more code ....
//we can now draw
vkCmdDraw(cmd, object.mesh->_vertices.size(), 1,0 , i);
我们正在 vkCmdDraw()
调用中使用循环中的索引来将实例索引发送到着色器。
现在我们已经实现了多种不同类型的缓冲区,以及在不同描述符集上的缓冲区。
本教程的最后一步是纹理,这将在下一章中介绍。但在进入那里之前,我强烈建议您尝试使用代码库做一些事情。
现在,我们每帧有一个描述符集用于 Set 0(相机和场景缓冲区)。尝试重构它,使其仅使用 1 个描述符集和 1 个缓冲区用于相机和场景缓冲区,将所有帧的结构体都打包到同一个统一缓冲区中,然后使用动态偏移。
或者,尝试创建另一个 SSBO,其中包含类似 ObjectColor 的内容,以在每个对象的基础上使用,并尝试使用它通过修改着色器以不同的方式为对象着色。
下一步:内存传输