使用Swift渲染Moana

2021-01-15 20:11:38

TLDR:在少于10.000行的Swift代码中渲染迪士尼的Moana场景。

沃尔特·迪斯尼动画工作室发布了莫阿纳岛的场景描述后,除了迪斯尼的Hyperion,还开始着手渲染。我知道以下渲染引擎:

在这里,我介绍由我编写的另一个Gonzales渲染器。它在很大程度上受到PBRT的启发,并用Swift编写(在C ++中有几行调用OpenEXR和Ptex)。它仅在能够在合理的时间内在免费的Google Cloud实例(8个vCPUS,64GB RAM)上呈现的情况下进行了优化。据我所知,这是唯一能够渲染不是用C / C ++编写的Moana的渲染器。我在Ubuntu Linux上使用vi和命令行Swift编写了代码,在macOS上使用Xcode编写了代码,因此在这些平台上进行编译应该相对容易些。

对于C和C ++中的头文件和预处理器,我总是感到不舒服。从我的角度来看,应该一次声明(而不是两次)声明(定义一个变量,一个函数,…)。同样,头文件的文本包含也带来了许多问题,例如必须向头文件添加实现细节(想到的模板),或者由于重复包含头及其组合爆炸而导致编译时间变慢。当我开始使用C ++模块时,我无法评估Python(太慢),Go(太像C)和其他一些东西,但最终只有Rust和Swift是真正的竞争者。由于可读性的缘故,我最终选择了Swift(我只是不喜欢“ impl trait”的“ fn main”)。另外,由LLVM和Clang的实现者编写的文件使我充满信心,它a)将来不会被放弃,并且b)达到我的性能目标。简而言之,我想要一种编译语言,没有指针,模块,概念,范围,可读模板,现在我想要它。同样,发明了编译器是通过使程序更具可读性来使程序员的生活更轻松,并且有时在查看基于模板的代码时,我认为我们会倒退。我喜欢我的东西可读。

解析经历了一些变化。首先,它是一个简单的String(file.availableData,编码:.utf8),但它太大了以适合内存。出于类似原因未使用数据。基金会的扫描仪也被逐出了一次。最后,我决定将InputStream读入UnsafeMutablePointer< UInt8>。数组64kB。

数组死角;简而言之,永远不要在热途中使用Array。也就是说,永远不要生成一个。从一开始就应该清楚这一点,因为它是堆分配的,但是由于它总是出现在用perf进行的分析的顶部,所以很快就学到了这一课。对于固定大小的数组,可以使用元组或Swift的内部FixedArray来克服。即使仅使用数组,下标getter也会显示在性能运行的顶部。

总的来说,我发现并行开发Linus和macOS相当实用,因为可用的用于检查性能和内存的工具可以很好地互补。我主要使用了四个工具:

性能:此Linux内核工具提供了宝贵的时间信息。只需将其启动,查看顶部显示的功能,并想知道时间浪费在哪里。暗示;它通常不在您认为的位置。在我的情况下,总是swift_retain或release一次又一次告诉您不要在堆上分配对象。

Valgrind Memcheck:这显示内存已耗尽。例如,使用此工具进行分析是将加速结构与加速结构生成器分离的原因;花在建立边界层次结构上的内存从未被释放过。在Swift中没有指针,没有malloc或new甚至没有shared_pointers都很好,但是仍然有必要考虑一下如何使用内存。

Xcode概要分析:我主要使用Time Profiler,Leaks和Allocations,它们为您提供与Perf和Valgrind大致相同的信息,但是从不同的角度来看。有时从两个不同的角度看同一件事很有帮助。这让我想起了过去,我们曾经将软件提供给三种不同的编译器(Visual Studio,GCC和IRIX的一种),它的名字又叫MIPSPro?

谈到内存,尽管Swift使得编写可读性和紧凑代码非常容易,但是您仍然必须考虑低级操作,例如内存分配等。我经常在结构和类之间切换,只是为了了解内存和性能如何受到影响。没有指针,new和shared_pointers的好处是,我大部分时间都可以在两者之间切换而无需更改系统中的任何其他内容。

关于基于协议的编程:翻阅当今的Gonzales,可以看到23种协议,57个结构,47个最终类和2个非最终类。继承几乎从未使用过。剩下的两个非最终类是TrowbridgeReitzDistribution和Texture,我都不满意,并且考虑在将来重新设计它们。总而言之,基于协议的编程最终会产生漂亮的代码,例如,我曾经拥有PBRT之类的Primitive类,但很快将其更改为继承自Boundable,Intersectable,Emitting(现已消失)等协议的协议。现在它也消失了,BoundingHierarchyBuild仅依赖于Boundable存在类型,并返回由BoundingHierarchy使用的Intersectables层次结构。现在,所有原语都存储为存在类型的数组,该数组由可绑定和可交叉的协议组成(var原语= [Boundable&Intersectable]())。

另一方面,BoundingHierarchy中的基元存储为[AnyObject&相交]。这有两个原因:1.仅需要交叉点。 2. AnyObject强制存储的对象为引用类型(或类),这节省了内存,因为结构和类的协议布局(OpaqueExistentialContainer)都使用40字节,因为Swift试图以内联方式存储结构,而纯类协议(ClassExistentialContainer)仅使用16个字节,因为只需要存储一个指针即可,如Swift的文档中所示或在源代码中进行了验证。我强调,这不仅是学术讨论,而且由于它出现在memcheck运行的顶部,所以我碰到了这一点。

您可以在少于10.000行中呈现Moana的原因之一是能够在Swift中编写紧凑的代码。一个极端的例子是参数列表。在PBRT中,您可以将任意参数附加到对象,从而在参数集[h | cpp]中产生大约1000行代码。在Swift中,您可以通过三行代码实现相同的目的:

实际上,我在这里有点作弊,但是您明白了。 (此外,我认为这在PBRT-v4中已经改变。)

关于将C ++用于Ptex和OpenEXR的接口支持:与C ++的互操作性正在Swift中发展,但在我开始时(或目前)还不可用。由于我仅将OpenEXR和Ptex用于读取纹理和写入图像,所以我求助于extern" C"。后来有一个modulemap和几行C ++代码(Ptex为100,OpenEXR为82)我支持读写OpenEXR图像和Ptex纹理。

我现在要发布代码,因为我可以在具有8个vCPU和64GB内存的Google Compute Engine上渲染Moana,这三个月都是免费的,因此请下载代码,并启动一个帐户。 🙂就是说,要做很多事情,因为我只对其进行了优化,以便能够渲染一张图像。以下是一个大型待办事项清单,从容易实施的项目到我将来可能会解决或可能不会解决的大型项目进行分类。

直接光线的光线差异。这应该相对容易些。看看PBRT-v3是如何做到的,如何在摄像机中实现差分生成,将其泵送到系统中,并在对Ptex的调用中使用。在那里它是自动处理的。

更好的层次结构:我仅实现了最简单的边界层次结构,这很不错,因为它只有177行代码,但渲染时间也不理想。在这方面,SAH优化的层次结构应该更好。由于我非常关注PBRT的实施,因此实施起来也不难。

更快的解析速度:集成Ingo Wald的快速pbrt解析器,该解析器可以在几秒钟内而不是半小时内解析Moana。甚至更好:在Swift中为pbf格式编写一个解析器。

关于更快的解析,层次结构生成和场景格式的想法:LLVM具有三种不同的位码格式。内存,机器可读(二进制)和人类可读的,它可以在这三种之间进行无损转换。我们可以一样吗?像PBRT(人类可读),PBF或USD(机器可读)和BHF(二进制层次结构格式)一样,它们的边界层次结构已经生成,可以简单地映射到内存中。

入门任务:我只是想让Moana进行渲染,但是增强Gonzales使其能够通过添加功能或修复错误来渲染其他场景应该相当容易。有很多场景可以尝试。也有许多PBRT出口商也应为冈萨雷斯服务。

内存:由于仅在渲染完成时才写入图像,因此许多内存用于像素样本。将其更改为在渲染时写入图块,并尽早丢弃样本。这会干扰像素过滤,但由于我们无论如何都要进行去噪,也许不再需要了吗?

较小的变换:到目前为止,变换存储两个矩阵,一个4×4矩阵存储变换及其逆矩阵。这有点浪费,因为您总是可以彼此计算,但是求逆很慢,但是在仔细考虑何时需要哪种转换之后,应该可以摆脱其中一种。现在在相交三角形时都使用了两者,但是是否可以在世界空间中存储三角形(以及其他对象,例如曲线)以摆脱射线到对象空间的转换以及类似地到表面相互作用的到世界空间的转换?以及如何与转换后的原语和对象实例进行交互?

降噪:我暂时正在使用OpenImageDenoise,但是在Swift中使用集成的去噪器当然是不错的选择。此外,应将美女,反照率和正常形象分别写入,这应该重新构造。

超越路径跟踪:查看PxrUnified并实现“引导路径跟踪”(我看过它,但看起来……令人迷惑)和流形下一个事件估计。我想我在某处看到了一个实现,但我忘了。 (如果只有Weta跟随迪斯尼的领导,并从那篇论文中发表了甘道夫的头像,那就叹气!)

渲染速度更快:Embree具有路径跟踪器。努力看待它,并尝试使冈萨雷斯更快。

GPU渲染:这应该是一个很大的渲染,显然,PBRT-v4会像上面提到的某些渲染器一样做到这一点。跟随它们并使用Optix在图形卡上进行渲染应该是非常有可能的,但是我更喜欢不涉及封闭源代码的解决方案。这意味着您必须实施自己的Optix。但是,看看CPU和GPU的发展情况,很可能在遥远的将来在两者上使用相同的(Swift)代码。您可以在云端拥有448个CPU的实例,而最新的GPU具有数千个微CPU,它们看起来越来越相似。我想知道将来是否需要对AVX进行编程,因为它似乎不需要了,因为您可以在问题上投入更多的内核。同时,内存变得越来越像NUMA,因此将数据放在ALU旁边变得越来越重要。也许有一天,我们在云中有渲染节点,每个渲染节点负责场景的一部分,每个节点将场景进行几何划分,并且仅将部分内容发送给CPUS。然后,返回的交叉点可以简单地按射线的t值排序,这让我想起了Chromium等排序优先/中间/最后的体系结构。

现在就这样。 我非常高兴收到评论,这些评论可以做得更好或更优雅地执行,还可以报告错误,甚至可以提出请求。 😉同时还要感谢Matt Pharr和PBRT,这是已知宇宙中最有价值的资源(至少在涉及渲染时)。