Link

我们在上一篇文章中留下了纹理,以便在这里更详细地介绍。

对于纹理,我们将使用 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();

引擎的基本功能现在已完成。您可以将此用作实现游戏的基础。但我们将在此章节中进行一些可选的调整,以提高其性能和质量。

下一步: 更快地绘制