【Heskey带你玩渲染】pbrt-v4中的wavefront path tracing
Wavefront Path Tracing
首先,老规矩:
未经允许禁止转载(防止某些人乱转,转着转着就到蛮牛之类的地方去了)
B站:Heskey0
注:本文需要CUDA和PBRT的知识,推荐书籍《CUDA C Programming》
pbrt第四版的书还没出,很多哥哥姐姐萌 (*^▽^*) 看代码的时候会很懵逼。因为中文网上还没有相关的博客,为了帮助大家理解,我写下这篇博客,帮助大家了解pbrt第四版的一个核心 —— wavefront
1. Introduction
我们可以通过CUDA,OpenCL这些接口实现在GPU上编程。在GPU上面编程和CPU有很大的不同,熟悉CUDA的都知道,在一个SM上同时可以并行32个线程
即使这样,GPU编程也会由很多难题。在这个线程束中:
- 有一部分线程中途挂掉了(if-else)
- 高带宽的CPU没有办法利用起来,GPU和CPU之间的内存拷贝会变得昂贵
- GPU的缓存贼小,kernel太大的话,很可能会冲爆缓存
为了尽可能避免这些难题,学术界的大佬萌就想出了一个办法 -> wavefront path tracing,这个方法被集成到了pbrt-v4中。这个方法的核心就是——把以往写得很大的kernel拆分成很多的小kernel。
2. 先分析一下老方法
老方法的kernel体量很大:
- 生成路径
- 对光源采样
- 不同材质的解决方案不同
在GPU的编程中,分支会导致线程资源不能够被充分应用。老方法中分支主要会出现在两个地方:
-
在采样path的时候,path随时会中断。在一个thread终止之后,线程束不会终止,也就是说,一个path中断之后其它的path还会跑,中断的path依然会占用线程资源。
-
一个线程束中的path命中不同材质的时候,会导致线程束中每个thread的逻辑不同。
3. Wavefront
3.1 使用路径池
wavefront path tracing方法中,维护了一个path池,这个池的大小为 \(1M(=2^{20})\) 个path,当path中断的时候,需要重新生成path,这个时候就可以去池子里面取。path的状态(path state)被存储到global memory(DRAM)中,每个path包括shadow ray和extension ray,占用212 bytes。1M个path,每个path占用212 bytes,所以总共就占用了212 MB的内存。(虽然把path池的内存提上去能够提升性能,但path池的内存太大的时候,性能的提升就不明显了,所以1M个path已经够用了)
3.2 拆分kernel
把kernel拆分到3个stages:
- logic stage
- material stage
- ray cast stage
3.2.1 Logic stage
logic stage只有一个kernel,这个kernel的任务就是推动path的前进:
- 计算light sample path和extension path的MIS权重
- 更新throughput
- 确定path是否终止了(Russian roulette“杀死”了ray,ray射出场景)
- 确定ray击中了什么材质
- 发送一条到material stage的请求
3.2.2 Material stage
每一堆消耗差不多的材质对应一个material kernel。比如,消耗大的就放在一个kernel,消耗小的放到另一个kernel。老方法中,所有的材质代码都在一个kernel里面,击中了不同材质,就用一个switch-case做分支。
3.3.3 Ray cast stage
ray cast kernel的任务就是投射出extension ray和shadow ray。与此同时,为了加载logic stage的输出结果,path state需要记录ray buffer中的索引
3.3 内存
wavefront formulation最大的缺点就是 —— path state 需要存储到内存中。那么就需要对memory layout做一些调整,使这个缺点变“弱”。对于GPU来说,使用SOA形式的memory layout会更加高效(使用SOA比AOS快了80%)。
3.4 Queue
通过为材质和光线投射阶段生成紧凑的请求Queue,确保每个启动的kernel总是能执行线程束中的所有线程。 这里的Queue是简单的预先分配的全局内存缓冲区,因此它们可以包含池中每个路径的索引。 每个队列在全局内存中都有一个item计数器,该计数器在写入队列时自动增加。 通过将项目计数器设置为零来清除队列。