我们稍后将进行适当的场景加载,但在我们到达那里之前,我们需要比矩形更好的网格。为此,我们将开始加载 GLTF 文件,但以非常简化和错误的方式,仅获取几何数据而忽略其他所有内容。
起始点存储库中附带了一个名为 basic.glb 的 glTF 文件。该文件包含一个立方体、一个球体和一个猴头网格,它们都位于原点中心。由于文件如此简单,因此可以正确加载它,而无需设置真正的 gltf 加载。
GLTF 文件将包含网格列表,每个网格上有多个图元。这种分离适用于使用多种材质的网格,因此需要多次绘制调用才能绘制它。该文件还包含场景节点的场景树,其中一些节点包含网格。我们现在将仅加载网格,但稍后我们将加载完整的场景树和材质。
我们的加载代码将全部位于 vk_loader.cpp/h 文件中。
让我们首先为加载的网格添加一些类,以及我们将需要的包含项。
#include <vk_types.h>
#include <unordered_map>
#include <filesystem>
struct GeoSurface {
uint32_t startIndex;
uint32_t count;
};
struct MeshAsset {
std::string name;
std::vector<GeoSurface> surfaces;
GPUMeshBuffers meshBuffers;
};
//forward declaration
class VulkanEngine;
给定的网格资源将具有从文件加载的名称,然后是网格缓冲区。但它还将具有 GeoSurface 数组,其中包含此特定网格的子网格。渲染时,每个子网格都将是其自身的绘制。我们将使用 StartIndex 和计数进行该绘制调用,因为我们将把每个表面的所有顶点数据附加到同一缓冲区中。
将这些包含项添加到 vk_loader.cpp。我们最终会需要它们
#include "stb_image.h"
#include <iostream>
#include <vk_loader.h>
#include "vk_engine.h"
#include "vk_initializers.h"
#include "vk_types.h"
#include <glm/gtx/quaternion.hpp>
#include <fastgltf/glm_element_traits.hpp>
#include <fastgltf/parser.hpp>
#include <fastgltf/tools.hpp>
我们的加载函数将是这样的
std::optional<std::vector<std::shared_ptr<MeshAsset>>> loadGltfMeshes(VulkanEngine* engine, std::filesystem::path filePath);
这是第一次看到 std::optional 被使用。这是一个标准类,它包装一个类型(此处的网格资源向量),并允许它出错/为空。由于文件加载可能因多种原因而失败,因此返回 null 是一个好主意。我们将使用 fastGltf 库,该库使用所有这些新的 stl 功能进行加载。
让我们从打开文件开始
std::cout << "Loading GLTF: " << filePath << std::endl;
fastgltf::GltfDataBuffer data;
data.loadFromFile(filePath);
constexpr auto gltfOptions = fastgltf::Options::LoadGLBBuffers
| fastgltf::Options::LoadExternalBuffers;
fastgltf::Asset gltf;
fastgltf::Parser parser {};
auto load = parser.loadBinaryGLTF(&data, filePath.parent_path(), gltfOptions);
if (load) {
gltf = std::move(load.get());
} else {
fmt::print("Failed to load glTF: {} \n", fastgltf::to_underlying(load.error()));
return {};
}
目前我们仅支持二进制 GLTF。因此,首先我们使用 loadFromFile
打开文件,然后我们调用 loadBinaryGLTF 来打开它。这需要父路径来查找相对路径,即使我们还没有它。
接下来,我们将循环遍历每个网格,将顶点和索引复制到临时 std::vector 中,并将它们作为网格上传到引擎。我们将从此构建一个 MeshAsset
数组。
std::vector<std::shared_ptr<MeshAsset>> meshes;
// use the same vectors for all meshes so that the memory doesnt reallocate as
// often
std::vector<uint32_t> indices;
std::vector<Vertex> vertices;
for (fastgltf::Mesh& mesh : gltf.meshes) {
MeshAsset newmesh;
newmesh.name = mesh.name;
// clear the mesh arrays each mesh, we dont want to merge them by error
indices.clear();
vertices.clear();
for (auto&& p : mesh.primitives) {
GeoSurface newSurface;
newSurface.startIndex = (uint32_t)indices.size();
newSurface.count = (uint32_t)gltf.accessors[p.indicesAccessor.value()].count;
size_t initial_vtx = vertices.size();
// load indexes
{
fastgltf::Accessor& indexaccessor = gltf.accessors[p.indicesAccessor.value()];
indices.reserve(indices.size() + indexaccessor.count);
fastgltf::iterateAccessor<std::uint32_t>(gltf, indexaccessor,
[&](std::uint32_t idx) {
indices.push_back(idx + initial_vtx);
});
}
// load vertex positions
{
fastgltf::Accessor& posAccessor = gltf.accessors[p.findAttribute("POSITION")->second];
vertices.resize(vertices.size() + posAccessor.count);
fastgltf::iterateAccessorWithIndex<glm::vec3>(gltf, posAccessor,
[&](glm::vec3 v, size_t index) {
Vertex newvtx;
newvtx.position = v;
newvtx.normal = { 1, 0, 0 };
newvtx.color = glm::vec4 { 1.f };
newvtx.uv_x = 0;
newvtx.uv_y = 0;
vertices[initial_vtx + index] = newvtx;
});
}
// load vertex normals
auto normals = p.findAttribute("NORMAL");
if (normals != p.attributes.end()) {
fastgltf::iterateAccessorWithIndex<glm::vec3>(gltf, gltf.accessors[(*normals).second],
[&](glm::vec3 v, size_t index) {
vertices[initial_vtx + index].normal = v;
});
}
// load UVs
auto uv = p.findAttribute("TEXCOORD_0");
if (uv != p.attributes.end()) {
fastgltf::iterateAccessorWithIndex<glm::vec2>(gltf, gltf.accessors[(*uv).second],
[&](glm::vec2 v, size_t index) {
vertices[initial_vtx + index].uv_x = v.x;
vertices[initial_vtx + index].uv_y = v.y;
});
}
// load vertex colors
auto colors = p.findAttribute("COLOR_0");
if (colors != p.attributes.end()) {
fastgltf::iterateAccessorWithIndex<glm::vec4>(gltf, gltf.accessors[(*colors).second],
[&](glm::vec4 v, size_t index) {
vertices[initial_vtx + index].color = v;
});
}
newmesh.surfaces.push_back(newSurface);
}
// display the vertex normals
constexpr bool OverrideColors = true;
if (OverrideColors) {
for (Vertex& vtx : vertices) {
vtx.color = glm::vec4(vtx.normal, 1.f);
}
}
newmesh.meshBuffers = engine->uploadMesh(indices, vertices);
meshes.emplace_back(std::make_shared<MeshAsset>(std::move(newmesh)));
}
return meshes;
当我们迭代网格中的每个图元时,我们使用 iterateAccessor 函数来访问我们想要的顶点数据。我们还在将不同的图元附加到顶点数组的同时正确构建索引缓冲区。最后,我们调用 uploadMesh 来创建最终缓冲区,然后我们返回网格列表。
使用 OverrideColors 作为编译时标志,我们将顶点颜色覆盖为顶点法线,这对于调试很有用。
Position 数组将始终存在,因此我们使用它来初始化 Vertex 结构。对于所有其他属性,我们需要检查数据是否存在。
让我们绘制它们。
std::vector<std::shared_ptr<MeshAsset>> testMeshes;
我们首先将它们添加到 VulkanEngine 类中。让我们从 init_default_data() 加载它们。将 #include <vk_loader.h>
添加到 vk_engine.h 包含列表中。
testMeshes = loadGltfMeshes(this,"..\\..\\assets\\basicmesh.glb").value();
在提供的文件中,索引 0 是一个立方体,索引 1 是一个球体,索引 2 是一个搅拌机猴头。我们将绘制最后一个,在绘制之前的矩形之后立即绘制它
push_constants.vertexBuffer = testMeshes[2]->meshBuffers.vertexBufferAddress;
vkCmdPushConstants(cmd, _meshPipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(GPUDrawPushConstants), &push_constants);
vkCmdBindIndexBuffer(cmd, testMeshes[2]->meshBuffers.indexBuffer.buffer, 0, VK_INDEX_TYPE_UINT32);
vkCmdDrawIndexed(cmd, testMeshes[2]->surfaces[0].count, 1, testMeshes[2]->surfaces[0].startIndex, 0, 0);
您将看到猴头有颜色。那是因为我们在加载器中将 OverrideColors
设置为 true,这将把法线方向存储为颜色。我们的着色器中没有适当的照明,因此如果您关闭它,您将看到猴头是纯白色。
现在我们有了猴头并且它是可见的,但它也是倒置的。让我们修复该矩阵。
在 GLTF 中,轴旨在用于 opengl,opengl 的 Y 轴向上。Vulkan 的 Y 轴向下,因此它是翻转的。我们在这里有两种可能性。一种是使用负视口高度,这是受支持的,并且会翻转整个渲染,这将使其更接近 directx。另一方面,我们可以应用一个翻转,将对象更改为投影矩阵的一部分。我们将这样做。
从渲染代码中,让我们为其提供更好的矩阵以进行渲染。在 draw_geometry()
上绘制网格的推送常量调用之前添加此代码
glm::mat4 view = glm::translate(glm::vec3{ 0,0,-5 });
// camera projection
glm::mat4 projection = glm::perspective(glm::radians(70.f), (float)_drawExtent.width / (float)_drawExtent.height, 10000.f, 0.1f);
// invert the Y direction on projection matrix so that we are more similar
// to opengl and gltf axis
projection[1][1] *= -1;
push_constants.worldMatrix = projection * view;
将 #include <glm/gtx/transform.hpp>
添加到 vk_engine.cpp 的顶部,以便我们可以访问这些转换函数。
首先,我们计算视图矩阵,该矩阵来自相机。现在,向后移动的平移矩阵就可以了。
对于投影矩阵,我们在这里做了一个技巧。请注意,我们正在向“近”发送 10000,向“远”发送 0.1。我们将反转深度,使深度 1 为近平面,深度 0 为远平面。这是一种大大提高深度测试质量的技术。
如果您此时运行引擎,您会发现猴头绘制时有点卡顿。我们尚未设置深度测试,因此头后侧的三角形可以渲染在前面之上,从而创建错误的图像。让我们去实现深度测试。
首先,通过在 VulkanEngine 类中添加一个新图像,与绘制图像并排,因为它们将在渲染时配对在一起。
AllocatedImage _drawImage;
AllocatedImage _depthImage;
现在我们将在 init_swapchain 函数中与 drawImage 一起初始化它
_depthImage.imageFormat = VK_FORMAT_D32_SFLOAT;
_depthImage.imageExtent = drawImageExtent;
VkImageUsageFlags depthImageUsages{};
depthImageUsages |= VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
VkImageCreateInfo dimg_info = vkinit::image_create_info(_depthImage.imageFormat, depthImageUsages, drawImageExtent);
//allocate and create the image
vmaCreateImage(_allocator, &dimg_info, &rimg_allocinfo, &_depthImage.image, &_depthImage.allocation, nullptr);
//build a image-view for the draw image to use for rendering
VkImageViewCreateInfo dview_info = vkinit::imageview_create_info(_depthImage.imageFormat, _depthImage.image, VK_IMAGE_ASPECT_DEPTH_BIT);
VK_CHECK(vkCreateImageView(_device, &dview_info, nullptr, &_depthImage.imageView));
深度图像的初始化方式与绘制图像相同,但我们为其提供了 VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT
用法标志,并且我们使用 VK_FORMAT_D32_SFLOAT
作为深度格式。
确保还将深度图像添加到删除队列中。
_mainDeletionQueue.push_function([=]() {
vkDestroyImageView(_device, _drawImage.imageView, nullptr);
vmaDestroyImage(_allocator, _drawImage.image, _drawImage.allocation);
vkDestroyImageView(_device, _depthImage.imageView, nullptr);
vmaDestroyImage(_allocator, _depthImage.image, _depthImage.allocation);
});
从绘制循环中,我们将深度图像从未定义状态转换为深度附件模式,就像我们对绘制图像所做的那样。这位于 draw_geometry()
调用之前
vkutil::transition_image(cmd, _drawImage.image, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL);
vkutil::transition_image(cmd, _depthImage.image, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL);
现在我们需要更改渲染通道开始信息以使用此深度附件并正确清除它。在 draw_geometry()
的顶部更改此项
VkRenderingAttachmentInfo colorAttachment = vkinit::attachment_info(_drawImage.imageView, nullptr, VK_IMAGE_LAYOUT_GENERAL);
VkRenderingAttachmentInfo depthAttachment = vkinit::depth_attachment_info(_depthImage.imageView, VK_IMAGE_LAYOUT_DEPTH_ATTACHMENT_OPTIMAL);
VkRenderingInfo renderInfo = vkinit::rendering_info(_windowExtent, &colorAttachment, &depthAttachment);
我们已经在 vkinit 结构上为渲染信息的深度附件留出了空间,但我们还需要将深度清除设置为其正确的值。让我们看看深度_附件_信息的 vkinit 的实现。
VkRenderingAttachmentInfo vkinit::depth_attachment_info(
VkImageView view, VkImageLayout layout /*= VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL*/)
{
VkRenderingAttachmentInfo depthAttachment {};
depthAttachment.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO;
depthAttachment.pNext = nullptr;
depthAttachment.imageView = view;
depthAttachment.imageLayout = layout;
depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
depthAttachment.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
depthAttachment.clearValue.depthStencil.depth = 0.f;
return depthAttachment;
}
它与我们用于颜色附件的类似,但我们将 loadOP 设置为清除,并将清除结构上的深度值设置为 0.f。如上所述,我们将使用深度 0 作为“远”值,深度 1 作为近值。
最后一件事是在管线中启用深度测试。我们在制作管线构建器时制作了深度选项,但将其禁用。现在让我们填写它。将此函数添加到 PipelineBuilder
void PipelineBuilder::enable_depthtest(bool depthWriteEnable, VkCompareOp op)
{
_depthStencil.depthTestEnable = VK_TRUE;
_depthStencil.depthWriteEnable = depthWriteEnable;
_depthStencil.depthCompareOp = op;
_depthStencil.depthBoundsTestEnable = VK_FALSE;
_depthStencil.stencilTestEnable = VK_FALSE;
_depthStencil.front = {};
_depthStencil.back = {};
_depthStencil.minDepthBounds = 0.f;
_depthStencil.maxDepthBounds = 1.f;
}
我们将关闭所有模板部分,但我们将启用深度测试,并将深度 OP 传递到结构中。
现在是从我们构建管线的地方使用它的时候了。在 init_mesh_pipeline
上更改此部分
//pipelineBuilder.disable_depthtest();
pipelineBuilder.enable_depthtest(true, VK_COMPARE_OP_GREATER_OR_EQUAL);
//connect the image format we will draw into, from draw image
pipelineBuilder.set_color_attachment_format(_drawImage.imageFormat);
pipelineBuilder.set_depth_format(_depthImage.imageFormat);
我们在构建器上调用 enable_depthtest 函数,我们为其提供深度写入,并将其作为运算符 GREATER_OR_EQUAL。如前所述,由于 0 是远,1 是近,因此我们只想在当前深度值大于深度图像上的深度值时才渲染像素。
也在 init_triangle_pipeline
函数上修改该 set_depth_format
调用。即使禁用了绘制的深度测试,我们仍然需要正确设置格式,以便渲染通道在没有验证层问题的情况下工作。
您现在可以运行引擎,猴头将正确设置。三角形和矩形的其他绘制在其后面渲染,因为我们没有为它们设置深度测试,因此它们既不写入也不从深度附件读取。
我们需要为我们正在创建的新网格添加清理代码,因此我们将在清理函数中,在主删除队列刷新之前销毁网格数组的缓冲区。
for (auto& mesh : testMeshes) {
destroy_buffer(mesh->meshBuffers.indexBuffer);
destroy_buffer(mesh->meshBuffers.vertexBuffer);
}
_mainDeletionQueue.flush();
在继续之前,删除背景中带有矩形网格和三角形的代码。我们将不再需要它们。删除 init_triangle_pipeline
函数、其对象以及在 init_default_data
上创建矩形网格及其用法。在 draw_geometry 上,移动视口和裁剪代码,使其位于调用 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, _meshPipeline);
之后,因为调用绑定三角形管线现已消失,并且需要在绑定管线的情况下完成那些 VkCmdSetViewport 和 VkCmdSetScissor 调用。
接下来,我们将设置透明对象和混合。
下一步: 混合