我们在上一篇文章中留下了纹理,以便在这里更详细地介绍。
对于纹理,我们将使用 stb_image 加载它们。这是一个单头文件库,用于加载 png、jpeg 和其他一些格式。遗憾的是,它不加载 KTX 或 DDS 格式,这些格式更适合图形用途,因为它们几乎可以直接上传到 GPU 中,并且是 GPU 直接读取的压缩格式,因此可以节省 VRAM。
fastgltf 在加载图像时有几种不同的可能性,因此我们需要支持它们。
让我们将逻辑写入 load_image 函数中
std::optional<AllocatedImage> load_image(VulkanEngine* engine, fastgltf::Asset& asset, fastgltf::Image& image)
{
AllocatedImage newImage {};
int width, height, nrChannels;
std::visit(
fastgltf::visitor {
[](auto& arg) {},
[&](fastgltf::sources::URI& filePath) {
assert(filePath.fileByteOffset == 0); // We don't support offsets with stbi.
assert(filePath.uri.isLocalPath()); // We're only capable of loading
// local files.
const std::string path(filePath.uri.path().begin(),
filePath.uri.path().end()); // Thanks C++.
unsigned char* data = stbi_load(path.c_str(), &width, &height, &nrChannels, 4);
if (data) {
VkExtent3D imagesize;
imagesize.width = width;
imagesize.height = height;
imagesize.depth = 1;
newImage = engine->create_image(data, imagesize, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_SAMPLED_BIT,false);
stbi_image_free(data);
}
},
[&](fastgltf::sources::Vector& vector) {
unsigned char* data = stbi_load_from_memory(vector.bytes.data(), static_cast<int>(vector.bytes.size()),
&width, &height, &nrChannels, 4);
if (data) {
VkExtent3D imagesize;
imagesize.width = width;
imagesize.height = height;
imagesize.depth = 1;
newImage = engine->create_image(data, imagesize, VK_FORMAT_R8G8B8A8_UNORM, VK_IMAGE_USAGE_SAMPLED_BIT,false);
stbi_image_free(data);
}
},
[&](fastgltf::sources::BufferView& view) {
auto& bufferView = asset.bufferViews[view.bufferViewIndex];
auto& buffer = asset.buffers[bufferView.bufferIndex];
std::visit(fastgltf::visitor { // We only care about VectorWithMime here, because we
// specify LoadExternalBuffers, meaning all buffers
// are already loaded into a vector.
[](auto& arg) {},
[&](fastgltf::sources::Vector& vector) {
unsigned char* data = stbi_load_from_memory(vector.bytes.data() + bufferView.byteOffset,
static_cast<int>(bufferView.byteLength),
&width, &height, &nrChannels, 4);
if (data) {
VkExtent3D imagesize;
imagesize.width = width;
imagesize.height = height;
imagesize.depth = 1;
newImage = engine->create_image(data, imagesize, VK_FORMAT_R8G8B8A8_UNORM,
VK_IMAGE_USAGE_SAMPLED_BIT,false);
stbi_image_free(data);
}
} },
buffer.data);
},
},
image.data);
// if any of the attempts to load the data failed, we havent written the image
// so handle is null
if (newImage.image == VK_NULL_HANDLE) {
return {};
} else {
return newImage;
}
}
函数很长,但实际上只是同一事物的 3 个版本。
第一个版本用于纹理存储在 gltf/glb 文件外部的情况,这很常见。在这种情况下,我们使用纹理路径调用 stb_load,如果成功,则创建图像。
第二种情况是当 fastgltf 将纹理加载到 std::vector 类型结构中时。如果纹理是 base64 编码,或者我们指示它加载外部图像文件,则会看到这种情况。我们获取字节并将它们发送到 stbi_load_from_memory。
第三种情况是从 BufferView 加载,当图像文件嵌入到二进制 GLB 文件中时。我们执行与第二种情况相同的操作,并使用 stbi_load_from_memory。
如果您尝试在此处编译引擎,您将看到 STB_image 函数缺少其定义。STB 是一个单头文件库,需要将宏包含到一个翻译单元中才能编译函数定义。将其添加到 vk_images.cpp 中。也可以是另一个 cpp 文件。这将使 stb_image 将函数的定义添加到此 cpp 文件中以进行链接。
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
让我们回到 gltf 加载函数并从那里加载图像。
// load all textures
for (fastgltf::Image& image : gltf.images) {
std::optional<AllocatedImage> img = load_image(engine, gltf, image);
if (img.has_value()) {
images.push_back(*img);
file.images[image.name.c_str()] = *img;
}
else {
// we failed to load, so lets give the slot a default white texture to not
// completely break loading
images.push_back(engine->_errorCheckerboardImage);
std::cout << "gltf failed to load texture " << image.name << std::endl;
}
}
我们尝试加载图像,如果加载成功,我们将图像存储到列表中。如果失败,我们使用错误图像。
如果您现在尝试再次运行项目,您将看到对象上有了纹理。
让我们填写 LoadedGLTF 上的 clearAll 函数,以便可以正确释放资源
void LoadedGLTF::clearAll()
{
VkDevice dv = creator->_device;
descriptorPool.destroy_pools(dv);
creator->destroy_buffer(materialDataBuffer);
for (auto& [k, v] : meshes) {
creator->destroy_buffer(v->meshBuffers.indexBuffer);
creator->destroy_buffer(v->meshBuffers.vertexBuffer);
}
for (auto& [k, v] : images) {
if (v.image == creator->_errorCheckerboardImage.image) {
//dont destroy the default images
continue;
}
creator->destroy_image(v);
}
for (auto& sampler : samplers) {
vkDestroySampler(dv, sampler, nullptr);
}
}
在开始时,我们销毁 descriptorPools 和共享材质缓冲区。然后我们循环遍历所有网格并销毁网格缓冲区,包括索引和顶点缓冲区。然后我们循环遍历图像,并销毁每个不是错误图像的图像(如上所示,如果加载失败,我们可能会使用错误图像。我们不想多次删除它)。
最后,我们循环遍历采样器并销毁每个采样器。
关于这一点的重要细节。您不能在 LoadedGLTF 被使用的同一帧内删除它。这些结构仍然存在。如果您想在运行时销毁 LoadedGLTF,要么执行像我们在 cleanup 函数中那样的 VkQueueWait,要么将其添加到每帧删除队列并推迟它。我们正在存储 shared_ptrs 以保存 LoadedGLTF,因此它可以滥用 lambda 捕获功能来执行此操作。
透明物体
我们之前省略了这些,但 gltf 文件不仅有不透明绘制,还有透明物体。当我们创建 GLTF 主材质并编译其管线时,我们为其启用了混合。我们已经在加载器中编写了将传递模式设置为透明的代码,但我们没有正确处理透明物体的渲染。
透明物体不写入深度缓冲区,因此如果绘制了透明物体,则可以在其顶部绘制不透明物体,从而导致视觉故障。我们需要移动透明物体,以便它们在帧的末尾绘制。
为此,我们可以对 RenderObjects 进行排序,但是透明物体的排序方式也与不透明物体不同,因此更好的选择是使 DrawContext 结构保存 2 个不同的 RenderObjects 数组,一个用于不透明物体,另一个用于透明物体。像这样分离物体对于各种原因非常有用,例如仅对不透明表面执行深度通道或其他着色器逻辑。将透明物体渲染到不同的图像中,然后将它们合成到顶部也很常见。
struct DrawContext {
std::vector<RenderObject> OpaqueSurfaces;
std::vector<RenderObject> TransparentSurfaces;
};
现在我们更改 draw_geometry
函数。由于我们需要从 2 个循环调用 vulkan 调用,我们将把内部绘制循环移动到一个 draw() lambda 中,然后从循环中调用它。
auto draw = [&](const RenderObject& draw) {
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, draw.material->pipeline->pipeline);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, draw.material->pipeline->layout, 0, 1, &globalDescriptor, 0, nullptr);
vkCmdBindDescriptorSets(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, draw.material->pipeline->layout, 1, 1, &draw.material->materialSet, 0, nullptr);
vkCmdBindIndexBuffer(cmd, draw.indexBuffer, 0, VK_INDEX_TYPE_UINT32);
GPUDrawPushConstants pushConstants;
pushConstants.vertexBuffer = draw.vertexBufferAddress;
pushConstants.worldMatrix = draw.transform;
vkCmdPushConstants(cmd, draw.material->pipeline->layout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(GPUDrawPushConstants), &pushConstants);
vkCmdDrawIndexed(cmd, draw.indexCount, 1, draw.firstIndex, 0, 0);
};
for (auto& r : mainDrawContext.OpaqueSurfaces) {
draw(r);
}
for (auto& r : mainDrawContext.TransparentSurfaces) {
draw(r);
}
通过这样做,我们现在有了正确的透明物体。如果您加载结构 gltf,您将看到光晕不再出现故障。
确保在函数末尾也重置透明绘制数组
// we delete the draw commands now that we processed them
mainDrawContext.OpaqueSurfaces.clear();
mainDrawContext.TransparentSurfaces.clear();
引擎的基本功能现在已完成。您可以将此用作实现游戏的基础。但我们将在此章节中进行一些可选的调整,以提高其性能和质量。
下一步: 更快地绘制