我们已经有了使用深度缓冲区和材质的任意网格渲染,但目前,每个对象都是硬编码的。我们将稍微重构渲染,以渲染对象数组。虽然这是一种非常简单的设置场景的方式,但它将允许您开始制作有趣的东西,例如简单的 3D 游戏。
它的工作方式是我们将有一个 RenderObject 结构体,它保存单个绘制所需的数据。网格、矩阵和材质。我们将有一个这些的数组,并按顺序渲染它们中的每一个。
为了存储材质和网格,我们将把它们存储在 unordered_map 中。unordered_map 的主要功能之一是它可以很好地保持指向对象的指针,因此我们可以拥有按名称划分的材质哈希图,然后只存储指向它们的指针。
材质现在只是一个 Pipeline 指针 + PipelineLayout。
vk_engine.h
//note that we store the VkPipeline and layout by value, not pointer.
//They are 64 bit handles to internal driver structures anyway so storing pointers to them isn't very useful
struct Material {
VkPipeline pipeline;
VkPipelineLayout pipelineLayout;
};
struct RenderObject {
Mesh* mesh;
Material* material;
glm::mat4 transformMatrix;
};
在 VulkanEngine 类中,让我们添加一些成员和函数来处理这些。
//add unordered_map to the headers on top
#include <unordered_map>
class VulkanEngine {
public:
//default array of renderable objects
std::vector<RenderObject> _renderables;
std::unordered_map<std::string,Material> _materials;
std::unordered_map<std::string,Mesh> _meshes;
//functions
//create material and add it to the map
Material* create_material(VkPipeline pipeline, VkPipelineLayout layout,const std::string& name);
//returns nullptr if it can't be found
Material* get_material(const std::string& name);
//returns nullptr if it can't be found
Mesh* get_mesh(const std::string& name);
//our draw function
void draw_objects(VkCommandBuffer cmd,RenderObject* first, int count);
};
以及它们的实现
Material* VulkanEngine::create_material(VkPipeline pipeline, VkPipelineLayout layout, const std::string& name)
{
Material mat;
mat.pipeline = pipeline;
mat.pipelineLayout = layout;
_materials[name] = mat;
return &_materials[name];
}
Material* VulkanEngine::get_material(const std::string& name)
{
//search for the object, and return nullptr if not found
auto it = _materials.find(name);
if (it == _materials.end()) {
return nullptr;
}
else {
return &(*it).second;
}
}
Mesh* VulkanEngine::get_mesh(const std::string& name)
{
auto it = _meshes.find(name);
if (it == _meshes.end()) {
return nullptr;
}
else {
return &(*it).second;
}
}
void VulkanEngine::draw_objects(VkCommandBuffer cmd,RenderObject* first, int count)
{
//empty for now
}
我们正在添加材质和网格的 2 个 map,以及一个 draw_objects 函数。我们没有单个 draw-mesh 函数。在渲染器中,几乎永远不会出现只渲染一个对象的情况,我们希望对函数进行排序,因此最好是我们的绘制函数接受一个要绘制的对象数组。
让我们移动我们的三角形和猴子网格,以便它们在 map 中注册,并确保它们的材质也已注册,以便我们可以在渲染时使用它们。
void VulkanEngine::load_meshes()
{
_triangleMesh._vertices.resize(3);
_triangleMesh._vertices[0].position = { 1.f,1.f, 0.5f };
_triangleMesh._vertices[1].position = { -1.f,1.f, 0.5f };
_triangleMesh._vertices[2].position = { 0.f,-1.f, 0.5f };
_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
_monkeyMesh.load_from_obj("../../assets/monkey_smooth.obj");
upload_mesh(_triangleMesh);
upload_mesh(_monkeyMesh);
//note that we are copying them. Eventually we will delete the hardcoded _monkey and _triangle meshes, so it's no problem now.
_meshes["monkey"] = _monkeyMesh;
_meshes["triangle"] = _triangleMesh;
}
在 load_pipelines 函数中,我们还将把最后创建的管线(网格管线)放入 map 中。
void VulkanEngine::init_pipelines()
{
//other code ....
VK_CHECK(vkCreatePipelineLayout(_device, &mesh_pipeline_layout_info, nullptr, &_meshPipelineLayout));
pipelineBuilder._pipelineLayout = _meshPipelineLayout;
//build the mesh pipeline
_meshPipeline = pipelineBuilder.build_pipeline(_device, _renderPass);
create_material(_meshPipeline, _meshPipelineLayout, "defaultmesh");
}
将材质和网格添加到 map 后,我们可以创建 renderobject。
我们将向 VulkanEngine 添加一个新函数 init_scene
,以创建一堆 renderobject,并在 init()
的末尾调用它
void VulkanEngine::init()
{
//other ....
init_pipelines();
load_meshes();
init_scene();
//everything went fine
_isInitialized = true;
}
现在,在我们的 init_scene 中,我们将创建一个猴子作为第一个对象,以及围绕它的更多三角形。很多三角形
void VulkanEngine::init_scene()
{
RenderObject monkey;
monkey.mesh = get_mesh("monkey");
monkey.material = get_material("defaultmesh");
monkey.transformMatrix = glm::mat4{ 1.0f };
_renderables.push_back(monkey);
for (int x = -20; x <= 20; x++) {
for (int y = -20; y <= 20; y++) {
RenderObject tri;
tri.mesh = get_mesh("triangle");
tri.material = get_material("defaultmesh");
glm::mat4 translation = glm::translate(glm::mat4{ 1.0 }, glm::vec3(x, 0, y));
glm::mat4 scale = glm::scale(glm::mat4{ 1.0 }, glm::vec3(0.2, 0.2, 0.2));
tri.transformMatrix = translation * scale;
_renderables.push_back(tri);
}
}
}
我们创建 1 只猴子,将其作为第一个东西添加到 renderables 数组中,然后我们在网格中创建许多三角形,并将它们放在猴子周围。
接下来,填充绘制函数。
void VulkanEngine::draw_objects(VkCommandBuffer cmd,RenderObject* first, int count)
{
//make a model view matrix for rendering the object
//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;
Mesh* lastMesh = nullptr;
Material* lastMaterial = nullptr;
for (int i = 0; i < count; i++)
{
RenderObject& object = first[i];
//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;
}
glm::mat4 model = object.transformMatrix;
//final render matrix, that we are calculating on the cpu
glm::mat4 mesh_matrix = projection * view * model;
MeshPushConstants constants;
constants.render_matrix = mesh_matrix;
//upload the mesh to the GPU via push constants
vkCmdPushConstants(cmd, object.material->pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(MeshPushConstants), &constants);
//only bind the mesh if it's a different one from last bind
if (object.mesh != lastMesh) {
//bind the mesh vertex buffer with offset 0
VkDeviceSize offset = 0;
vkCmdBindVertexBuffers(cmd, 0, 1, &object.mesh->_vertexBuffer._buffer, &offset);
lastMesh = object.mesh;
}
//we can now draw
vkCmdDraw(cmd, object.mesh->_vertices.size(), 1, 0, 0);
}
}
我们首先计算相机本身的矩阵。然后我们迭代 renderables 数组中的每个对象,并按顺序渲染它们中的每一个。现在的循环是一个简单的循环,没有排序,但是当 renderables 数组中的对象已经排序时,就没有太多排序的需要。如果需要,可以按管线指针对 renderables 数组进行排序。请注意我们如何在 BindVertexBuffers 和 BindPipeline 调用中检查 lastMesh 和 lastMaterial。无需在绘制之间一遍又一遍地重新绑定相同的顶点缓冲区,并且管线是相同的,但我们正在每次调用时推送常量。这里的循环比您想象的性能要高得多。这个简单的循环将渲染成千上万个对象而没有问题。绑定管线是一个昂贵的调用,但是使用不同的推送常量一遍又一遍地绘制同一个对象非常快。
最后一件事是用调用此函数的代码替换 draw()
函数中的旧代码。它应该像这样结束。
vkCmdBeginRenderPass(cmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE);
draw_objects(cmd, _renderables.data(), _renderables.size());
//finalize the render pass
vkCmdEndRenderPass(cmd);
现在我们已经更改了代码,您可以尝试从 Vulkan Engine 类中删除 _triangleMesh、_monkeyMesh 及其硬编码管线。我们不再需要任何这些,因为我们现在有一个简单的系统来管理网格和管线。
随意尝试在 load_scene 函数中创建的三角形和猴子的数量,或更改它们的生成方式。如果您禁用调试层并在发布模式下运行,您会发现您可以达到数十万个对象计数,然后才会变慢。在带有层的调试模式下,它不会那么快。
现在我们有了一个实际可以做事的引擎。您可以尝试做一些练习。
- 从 VulkanEngine 类中清理所有硬编码的管线和网格
- 使用更新的着色器创建多个管线,并使用它们渲染每个具有不同材质的猴子
- 加载更多网格。只要它是带有 TRIANGLE 网格的 obj,它就应该可以正常工作。确保在导出时 obj 包括法线和颜色
- 向相机添加 WASD 控件。为此,您需要修改绘制函数中的相机矩阵。
- 在渲染之前按管线和网格对 renderables 数组进行排序,以减少绑定次数。
在章节的 github 存储库中,chapter-3-scene 将完成完整的重构。但不是其他可选功能。
下一步:双缓冲