实例化

更新记录
  • 2023/9/1 增加该扩展文档

  • 2023/9/1 增加 教程 章节

  • 2023/9/1 增加 多实例 章节

  • 2023/9/1 增加 多物体 章节

  • 2023/9/4 更新 多实例 章节

  • 2023/9/4 增加 设备内存分配器 (DMA) 章节

  • 2023/9/4 增加 hello_vulkan.h 章节

  • 2023/9/4 增加 hello_vulkan.cpp 章节

  • 2023/9/4 增加 结果 章节

  • 2023/9/4 增加 VMA :Vulkan 内存分配器 章节

  • 2023/9/4 VMA :Vulkan 内存分配器 章节下增加 hello_vulkan.h 章节

  • 2023/9/4 VMA :Vulkan 内存分配器 章节下增加 hello_vulkan.cpp 章节

  • 2023/9/7 提供 Turbo 实现开源示例

  • 2023/9/7 更新 多实例 章节,增加 注解

  • 2023/9/7 更新 多物体 章节,增加 注解

文献源

Turbo 引擎中对该教程的实现示例

Turbo 引擎对该教程进行了实现,具体如下:

../../../_images/NVIDIA_instances.png

实例化结果示意图

教程

该教程为 Vulkan 光线追踪教程 的扩展。

光线追踪可以一次性处理有很多物体的实体。对于一个实体,一个顶层加速结构中可以包含不同的底层加速结构。然而,当我们有很多不同的物体时,在内存分配上就会遇到问题。很多 Vulkan 驱动实现只支持最多 4096 个分配,当我们的程序每个物体创建 4 次分配(顶点,索引和材质),其中还包括一个底层加速结构。这就意味着当我们创建 1000 个物体时,这将会触及到分配上限。

多实例

注解

该方式实现的多实例的本质是通过创建多个 VkAccelerationStructureInstanceKHR 并引用相同的底层加速结构(模型)实现的。

首先,让我们先来看看使用零星的几个物体构建多个实例的场景长什么样。

main.cpp 中,增加如下头文件:

#include <random>

之后将 main() 中的 helloVk.loadModel 调用更改如下,这将会创建 cubecube_multi 的实例。

// 加载模型
helloVk.loadModel(nvh::findFile("media/scenes/cube.obj", defaultSearchPaths, true));
helloVk.loadModel(nvh::findFile("media/scenes/cube_multi.obj", defaultSearchPaths, true));
helloVk.loadModel(nvh::findFile("media/scenes/plane.obj", defaultSearchPaths, true));

std::random_device              rd;  // 用于获取用于随机引擎的种子
std::mt19937                    gen(rd());  // rd() 的标准 mersenne_twister_engine 种子
std::normal_distribution<float> dis(1.0f, 1.0f);
std::normal_distribution<float> disn(0.05f, 0.05f);

for(uint32_t n = 0; n < 2000; ++n)
{
  float         scale = fabsf(disn(gen));
  nvmath::mat4f mat =
      nvmath::translation_mat4(nvmath::vec3f{dis(gen), 2.0f + dis(gen), dis(gen)});
  mat              = mat * nvmath::rotation_mat4_x(dis(gen));
  mat              = mat * nvmath::scale_mat4(nvmath::vec3f(scale));
  helloVk.m_instances.push_back({mat, n % 2});
}

备注

这将会创建 3OBJ 模型和相应的实体,之后将会随机创建 2000 个绿色或各面异色的方盒实例。

小技巧

此种方式是通过创建少数底层加速结构作为几何物体,之后创建大量顶层加速结构的实体来实现的实例化。

多物体

注解

该方式实现的多实例的本质是通过创建多个底层加速结构(模型)实现的。当创建的底层加速结构过多时大概率会触及 Vulkan 资源分配的上限。

创建多个物体,而不是创建多个实例。

将上面的代码替换成如下:

std::random_device              rd;  // 用于获取用于随机引擎的种子
std::mt19937                    gen(rd());  // rd() 的标准 mersenne_twister_engine 种子
std::normal_distribution<float> dis(1.0f, 1.0f);
std::normal_distribution<float> disn(0.05f, 0.05f);
for(int n = 0; n < 2000; ++n)
{
  float         scale = fabsf(disn(gen));
  nvmath::mat4f mat   = nvmath::translation_mat4(nvmath::vec3f{dis(gen), 2.0f + dis(gen), dis(gen)});
  mat                 = mat * nvmath::rotation_mat4_x(dis(gen));
  mat                 = mat * nvmath::scale_mat4(nvmath::vec3f(scale));

  helloVk.loadModel(nvh::findFile("media/scenes/cube_multi.obj", defaultSearchPaths, true), mat);
}

helloVk.loadModel(nvh::findFile("media/scenes/plane.obj", defaultSearchPaths, true));

这样也是可以工作的,但是在加载 1363 个物体之后将会输出如下错误。 创建 1363 个之后的所有物体将会失败。

Error

Error: VUID_Undefined

Number of currently valid memory objects is not less than the maximum allowed (4096).

Note

This is the best case; the application can run out of memory and crash if substantially more objects are created (e.g. 20,000)

小技巧

此种方式是通过创建大量数底层加速结构作为几何物体,之后创建少数的顶层加速结构的实体来实现的实例化。

设备内存分配器 (DMA)

如上分配问题可通过使用内存分配器得到解决。

hello_vulkan.h

hello_vulkan.h , 在文件顶部增加如下宏定义,用于声明使用何种分配器。

// 选择使用何种分配器
#define ALLOC_DMA
//#define ALLOC_DEDICATED
//#define ALLOC_VMA

替换缓存和纹理的定义并包含正确的分配器。

#if defined(ALLOC_DMA)
#include <nvvk/memallocator_dma_vk.hpp>
using Allocator = nvvk::ResourceAllocatorDma;
#elif defined(ALLOC_VMA)
#include <nvvk/memallocator_vma_vk.hpp>
using Allocator = nvvk::ResourceAllocatorVma;
#else
using Allocator = nvvk::ResourceAllocatorDedicated;
#endif

并将 ResourceAllocatorDedicatednvvk:: 替换成通用分配类型。

Allocator m_alloc;

hello_vulkan.cpp

在源文件中不需要做任何修改,所有的分配器都使用相同的 API

结果

相对于成千的分配,该示例仅使用 14 个内存分配。

备注

下图所示的其中一些分配是 Dear ImGui 分配的,并不是 DMA 分配的。其中的 14 个蓝边框的内存为 DMA 分配的。

../../../_images/VkInstanceNsight1.png

内存分配示意图

最后,在 Nsight GraphicsVulkan 的设备内存如下:

../../../_images/VkInstanceNsight2.png

Vulkan 设备内存示意图

VMA :Vulkan 内存分配器

我们同样可以使用 AMDVulkan Memory Allocator ( VMA )。

VMAnvpro_core/third_party 下的一个子模块。

VMA 使用的是专用内存,所以您需要在 main.cpp 增加如下扩展来创建上下文。

contextInfo.addDeviceExtension(VK_KHR_BIND_MEMORY_2_EXTENSION_NAME);

hello_vulkan.h

激活 VMA 的宏定义

#define ALLOC_VMA

hello_vulkan.cpp

VMA 需要获取函数的具体实现并且在之后的代码中只允许被定义一遍,并且需要在 #include "hello_vulkan.h" 之前定义:

#define VMA_IMPLEMENTATION

为确认是否使用 VMA 分配器,在 VMAMemoryAllocator::allocMemory() 处打个断点。