该工程已经开源:

GitHub - Puluomiyuhun/PL_Tracer: This is a ray tracing renderer based on optimx and cuda.

引言

之前我们使用纯C++写过一个光线追踪渲染器。尽管我们使用了BVH优化求交,并引入了基于cosine项的重要性采样优化,但是跑光追的效率依旧堪忧:当场景引入面数极多的模型、brdf较复杂的材质后,采样数往往需要提升至1000+甚至10000+,这样渲染一张场景图需要数十个小时。这样的渲染时间显然是无法接受的,因此我决定引入杀手锏:将渲染工作从cpu转到gpu上,即将软件光追转化成硬件光追。

提到硬件渲染,大家一定会想到cuda、opengl和dx这些图形接口,本来我想直接套dx的硬件光追,但恰巧最近在看siggragh的时候发现了optix这个宝藏sdk,它对于复杂求交、加速结构和cuda部分的封装十分到位,最重要的是 作者为了照顾新人,编写了一系列optix光追的教程:GitHub - ingowald/optix7course,学习难度曲线相对平缓(乐,实际上也未必简单到哪去,看过官方案例的都知道成熟的optix项目都是cuda+opengl+optix的混合编程)。从本篇博客开始,我会大体按照optix7course的教程进行渲染器的推进,并对于一些代码提出自己的理解和思考;同时会自主引申出自定义场景、相机、复杂材质等教程中没有涉及的内容,从而完成一个功能充足、效率可观的光追渲染器。

Part0. 硬件光追原理

1、光追渲染管线

首先回忆我们之前在C++光追渲染器中描述的软件光追:从相机视口的某个像素发射一根射线,射线与场景中的三角形或层次包围盒(BVH)进行求交,一直找到最近的求交点,然后计算bsdf的着色以及光线的弹射方向;如果没有找到交点,那么就赋予一个底色(一般是环境贴图或是纯色)。

实际上硬件光追的算法逻辑与软件光追无异,同样需要发射射线,需要在场景中快速求交,然后分别讨论求交失败和求交成功找到最近交点的着色情况。不同的是,为了能在硬件GPU上形成高并行的流水计算,硬件光追也定义了一套渲染管线,这套渲染管线上也装载着若干个着色器,从而实现像光栅化渲染管线那样大规模的GPU并行计算。

当然,光追的管线和光栅化的管线差距还是很大的,光追管线上的着色器主要是这五类:

1、RayGen Shader(负责从像素中发射光线,并将该像素的最终结果写回)

2、Miss Shader(处理求交失败时的着色情况)

3、Any Shader(用户定义某个交点是否需要抛弃,比如纯透明物体的求交就没有意义,就需要抛弃)

4、Intersection Shader(定义非三角形形状的求交算法)。

5、Closet Shader(处理最近求交点的着色情况)

下面用一张示意图来描述这个过程:

上图是一个粗略的逻辑图,我再描述一下这个状态机:RayGen Shader负责创建每个像素出射的光线,然后Optix底层就会开始进行最近点的求交,如果遇到非三角形形状物体就需要Intersection Shader来定义求交规则,每次求交成功了就把求交的点存进Any Hit。Any Hit中定义了用户是否认可这个最近点(有时候用户可能认为某个最近交点不合适,比如透明物体的求交就是无效求交,需要强行抛弃),如果认可该交点,就保存为当前的最近交点。当求交任务完成后检测一下是否有交点,如果没有则调用Miss Shader来渲染求交失败后的颜色;如果有交点则通过Closest Hit得到那个最近交点,并完成求交光线的颜色计算,给出下一次弹射的方向。注意此时的弹射本质上是发射了一根新光线,所以还是通过RayGen Shader来管理发射,然后Optix底层就会进行下一次求交,依次类推……这里的逻辑和之前我们写的c++光追是几乎一样的,只是很多步骤都被封装到着色器里 使用GPU来计算了,这也是Optix等能够把光追效率提升成千上万倍的根本原因。

下图是一个详细的pipeline过程,描述了这几个Shader的逻辑关系: