Cycles、实例化、和内存的那些事

各位好,今天我们来聊聊渲染中的内存调用机制,如何优化使用内存以及避免 Blender 内存溢出。其实这篇文章是我这周在解决草地渲染时,考虑如何解决内存调用优化后整理完成的,所以如果你也希望制作一个草原这样的大场景,本文应该会有些帮助。

线股 VS 网格粒子

这是本文要讨论的基础对象,所以首先我们先分别了解下两者区别。

线股是一种原生的角色毛发制作模型,允许一些修饰性操作,例如拧在一起等,由于其原理还是粒子系统,所以相当地耗费内存。这是因为,在渲染的时候,所有的粒子都必须按照最终造型和精度计入内存,而不是我们在视图中看到的缩略线股,因此这样对内存的消耗是巨大的。但是还没有完,接下来所有的线股分段(也就是为毛发做细分等级的精度指标)将全部缓存在 BVH 树中(BVH 是一种边界层次树结构,主要用于 Cycles 优化光线的交互检测计算)。

虽然相比较高分辨率的贴图等资源,BVH 树本身的体积很小,但是 BVH 的结点树却直接影响着渲染的性能。也就是说,结点越多,光线反射交叉的计算序列也越长。那么在草地中,由于草的线股量较大,所以 BVH 的体积也会相当庞大,这会导致计算量加倍。

Cycles、实例化、和内存的那些事

没有实例化的 BVH 树

但是当你在使用粒子复制,或者其他类似于复制组物体来共享某个网格物体的数据块时,这个数据块是被实例化过的,这意味着,被复制的物体本身拥有独立的 BVH 树,而仅需要被主干 BVH 树引用即可,内存空间因此可以被最大限度地节省,这是种相当高效的方式,对应的制作方法包括:复制组,复制顶点,复制面和粒子等。

Cycles、实例化、和内存的那些事
BVH 树引用其他 BVH 树为支叶

你也许会问:那么渲染时间呢?但结果不是这么简单的因果关系,这涉及你的场景设计等,所以基本上这里会有两种情况:

  1. 由于网格的 BVH 已经单独实例化了,所以场景很清晰地预先知道网格的边界,并且跳过对应网格的交叉计算。
  2. 但是也有另外一种可能,那就是这个网格可能会被其他物体给切分成几块,那么当光线进行交叉计算时,会突然发现,“啊,这个实例化的物体和光线原点真近啊,要不试试检测一下这个物体”,于是所有的计算量都会花在对这个实例化 BVH 物体做交叉检测上,但是稍后系统才会意识到,原来这个检测了半天的物体只是整个网格的一小部分,另外一个部分离当前光源点还很远呢。

虽然大部分情况是第一种情况,但是后面这种情况也会时常出现,我想说的是,对于一些特别复杂的场景,还是第一种情况会常见一点。

所以结论就是:尽可能使用实例化预处理,因为可以帮忙节省内存,并且提升复杂场景的计算性能。

实例化粒子修改器

我想有必要单独说说这个部分,因为我发现很多艺术家将这个功能想的太复杂了。

事实上,虽然这个修改器在名字上加了“实例化”三个字,但是他和 BVH 的实例化计算一点关系都没有。这个修改器的作用是将父级粒子系统的坐标点复制下来,然后应用给其他子物体的坐标位置上。(不过事实上会更复杂,因为还包含旋转等属性,但还好增加的计算量不会影响整体的性能。)结果是你会得到相当高精度的模型,但却只是简单地将所有网格物体都一一匹配到粒子上,并且是真正的网格,所以完全没有一点实例化处理。

所以请尽可能地不用这个修改器吧,特别是你希望在一个复杂场景中对某个物体实例化成千上万次时。

锁定界面渲染

Cycles、实例化、和内存的那些事

在渲染面板上有个锁定按钮,他的作用有两个:

  1. 避免视图更新和渲染计算在线程上的冲突,特别是在渲染动画时可能会导致系统崩溃。
  2. 释放视图绘制中调用的内存,因为这些可以借调给渲染引擎使用。

即使这个选项是开启状态,你也可以预览画面(平移甚至缩放),或者单击 ESC 停止渲染,而其他界面会保持锁定。

所以,就目前的设计上看,在内存的优化上还是很保守的,并不会冒险释放大量占用内存给渲染使用,所以这也留下了很大空间让我们来改进。

内存碎片

这段应该是本文技术要点最多的部分了,不过我会尽可能地简单描述他们。

基本上,Blender 的内存分配和释放机制很简单,特别是在渲染时。所以内存会被分割为多个碎片,分别按照事务顺序保存和释放,而释放的部分是不需要考虑其在内存地址中的位置,这样就会导致内存地址间会产生空格区域,除非操作系统决定分配其他任务到这里,否则会向更高的地址分配新任务,所以你最终会得到一个十分糟糕的内存分配结果。

Cycles、实例化、和内存的那些事内存碎片:注意到新的数据块可能会增加整体的内存消耗

非常不幸的是,我们没办法从 Blender 的层面上解决这个问题。

可幸运的是,在 Blender 的 Linux 版本中,我们用到了一个库名叫 jemalloc,他可以修改内存的分配机制,在处理内存碎片上会更合理。

在其他平台上还没有测试过这个库的效果,所以目前只能适用于 Linux。

视图衍生的网格

首先,衍生网格是一种内部数据结构,为模型在应用修改器等操作后的最终多边形结构,所以你可以想象他就是一个最终的高模。

在渲染的时候,渲染管线会确保场景的计算状态,确保需要的物体都被包含了进来,所有动画动作以及应用至物体的坐标位置上。但与此同时,系统还会为视图创建一个衍生网格,这其实是一个遗留设计,例如顶点关系、缩进和布尔运算的核心计算都需要基于这些代码,因此是无法避免的额外计算。

这些衍生网格会一直保留在内存中,直到渲染结束,而没有什么办法可以移除它们。

你也许会想,“哦,那么新的独立视图设计会解决这个问题吗?”很不幸,答案是不能,因为这和独立视图没有一点关系,这是渲染管线的问题。我们会在独立视图的更新上考虑这点,并做一些优化,测试结果显示还行。

粒子系统缓存

和纯代码上的问题不同,粒子系统缓存量算大吗?当你在更新粒子物体时,其实后台发生了以下事情:

  1. 粒子系统的修改器会根据上一个修改器来重新缓存衍生网格(如果在修改器栈中粒子系统处于优先位置上),所以如果你的模型是高模,或者在粒子系统之前拥有一个细分修改器,那你可能会被吞食掉大量的内存。
  2. 同时,粒子系统还会缓存所谓的粒子路径,这些是用于视图和渲染引擎做视觉查看的部分,这也会占用掉大量内存。

当然还有一些代码上的工作,需要一点时间,但至少不是大问题。

细分表面

在次之前,开放细分的集成工作还没有完,我们依然使用的是老代码,所以这还遗留着以下问题:

  1. 内部使用的是一种内存竞技分配模式,也就是说大量的小型内存会优先抢占常规内存的空间,不幸的是,这还不支持内存的释放,这样会导致极高的内存占用。对于艺术家来说,这些计算可能是较低层,虽然可以在被修改器计算完成后一口气释放,但是获取内存的占用量还是很大的。
  2. 由于一些历史原因,这些修改器会在渲染期间从较为节省的衍生模型模式,切换至快速易于渲染而不那么节约的模式,这些代码需要被重构。

Cycles 部分

在 Cycles 这部分又是另外一种情况了,特别是在 Cycles 从 Blender 中导出模型时:

  1. 在渲染每一个物体时,Cycles 会依次检索基于渲染精度的衍生模型,并且拷贝这些物体的数据到它自己的数据结构中。
  2. 正因为这种交互机制,导致衍生模型会被转换为实际模型规模的数据块,然后再导入至 Cycles 中,这些转换过程会吃掉一部分的内存。
  3. 接着 Cycles 会询问 Blender 去重组一些 Ngon 面,这样也会导致一些内存消耗。
  4. 当 Cycles 的数据结构完成转换后,会构建基于此模型的 BVH 树,那么所有的模型都会被打包进 BVH 并准备提交给渲染任务,而期间这些模型数据将保留相同的数据结构,因为这样才能同步更新至视图显示上,但在后面渲染计算开始之后,这些消耗可能会降低下来。

但由于在 Cycles 这边对内存调用的释放并不能解决草原镜头的缓存制作,但这些结论在经过一些测试后都是很值得参考的,不过我们依然要解决 Cycles 对内存的调用方式。

结论

写得多了,收一下:

  • 尽可能的实例化对象,因为这会降低内存消耗,加速渲染速度。
  • 如果你在 Linux 下使用 Blender,务必考虑使用 jemalloc 库。
  • 还有一些是代码层面上的问题,这些都需要时间。

好吧,希望本文能有所启发,谢谢!


原文链接