Link

我们已经有了使用深度缓冲区和材质的任意网格渲染,但目前,每个对象都是硬编码的。我们将稍微重构渲染,以渲染对象数组。虽然这是一种非常简单的设置场景的方式,但它将允许您开始制作有趣的东西,例如简单的 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 将完成完整的重构。但不是其他可选功能。

下一步:双缓冲