人气 1061

[游戏程序] 计算机图形学入门:什么是光线追踪? [复制链接]

九艺网 2018-10-18 18:49:51

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?注册

x
142612x9mmp9yzum9mmsx9.jpg


文/洛城来源:知乎
原文:https://zhuanlan.zhihu.com/p/41269520

从年初GDC放出DXR的消息已经有很长一段时间了(最初接触DXR的相关API还是在NVIDIA工作的时候,转眼大半年过去,我已经离开了NVIDIA),这是一篇基于我对光线追踪的了解写的入门文章(因为我本人也只是入门水平)。文中会少量涉及DirectX Raytracing的相关API,大部分还是阐述光线追踪技术的基本原理,在末尾的部分会recap今年GDC上的两篇[1][2]有关光线追踪的演讲。希望不太熟悉这个领域又时常听说光线追踪这个概念的同学能够通过这篇内容粗略地了解整个光线追踪技术的框架。

这篇内容会覆盖的概念包括渲染方程,蒙特卡洛积分,重要性采样,低差异序列,路径追踪,包括用实时光线追踪实现贴图采样,软阴影,AO,间接反射,间接漫射,半透明渲染和次表面散射。由于大部分内容都是概念性介绍,我会在文章的末尾把我看过/部分看过的书籍文献列出来,以便大家深入学习。

什么是光线追踪?

这是一个很难回答的问题,它的概念根据上下文不同,可能是某个具体的实现技术,也可能是一个框架的总称。一般来说,我们总是喜欢把它和另一个概念:光栅化渲染作为相对的两个概念。

熟悉实时图形学的同学都知道,现代的游戏渲染引擎都是以光栅化渲染为基本框架搭建的:一个复杂的场景的渲染任务会以物体为单位划分为若干个子任务,每个物体由若干三角面组成,我们将这些三角面经过几何变换映射到屏幕的某些区域,然后将三角面覆盖的区域拆解成一个个的像素,这个拆解的过程就叫做光栅化。在这个逐层拆解的过程中,下一层就会失去对上一层全局信息的了解,比如拆分成物体后,我们就不知道场景里其他物体的存在了,拆分成三角面后,我们就无法得知其他三角面的信息,到对每个像素进行着色时,我们连该像素所在的三角形的信息也丢失了。这样拆解任务可以让渲染过程高度并行化,所以非常的快,但是同时因为全局信息的丢失,我们很难实现一些需要全局信息的渲染效果。

与之相对的,近年来渲染技术的发展,多半是围绕全局照明效果的,譬如环境光遮蔽,间接反射,漫射等。这些技术五花八门,实现方法完全没有一个统一的框架可以遵循,也导致了近年来游戏引擎的设计越来越复杂。而早在上个世纪,在离线渲染领域,这些技术就已经有了统一的解决框架,即光线追踪。

与光栅化渲染不一样的是,光线追踪把一个场景的渲染任务拆分成了从摄像机出发的若干条光线对场景的影响,这些光线彼此不知道对方,但却知道整个场景的信息。每条光线会和场景并行地求交,根据交点位置获取表面的材质、纹理等信息,并结合光源信息计算光照。

142704e2l68qxfz89dx9uu.jpg
一个典型的光线追踪场景

管线

不论是光栅化框架还是光线追踪框架,一个三角面/一根光线的整个绘制过程都可以划分为若干个阶段,这些阶段合在一起就是我们通常意义上说的管线(Pipeline)。一个典型的光栅化管线(Raster Pipeline)如图:

141900qbbgg4q4b2mqnbqm.jpg
Raster Pipeline

这样设计的好处是一个渲染任务变得模块化并且耦合度较低,便于硬件及底层的图形API设计。可以看到,图形管线中有部分的阶段是所谓的固定管线,而另外一部分则是可编程管线。固定管线完成一些固定的任务,比如顶点的获取/剔除,顶点数据的插值,深度/模板的剔除等,这些模块只能通过一些配置参数来进行控制;而可编程管线则允许用户使用自定义的着色器(Shader)对数据(顶点,面,像素)进行处理。关于光栅化管线的细节可以看NVIDIA的这篇文章:Life of a triangle-NVIDIA's logical pipeline。

近年来GPU因为在并行计算上的优势越来越多地被用在了通用计算(GPGPU)的领域,因此在原有光栅化管线的基础上,也就引申出了计算管线(Compute Pipeline)。类似的,DXR API实际上就是定义了一种适用于光线追踪的Raytrace Pipeline。

渲染方程

渲染方程[3]回答了这些问题:即究竟什么是渲染?一个表面上某一点的亮度/颜色究竟如何计算?通常我们看到的渲染方程形式是这样的:

141903zfkyyckuzug2y6yk.jpg

一个表面的亮度(这里不打算去解释Irradiance/Radiance/Flux这些物理概念,大家仅做直观理解即可,想要进一步去了解这些概念可以去看相关资料[4][5])是由两个部分组成的,一部分是表面的自发光(上述公式中的),另一部分是其他表面的射向该表面的光线(上述公式中的)经过BRDF(上述公式中的)作用后的结果。BRDF描述的是表面本身的性质,比如它的光滑程度,导电程度等等。由于四面八方的光线都会作用在这个表面,所以我们需要对所有方向进行积分,也就是一个球面上的积分,考虑到积分项中的,那么只有位于正半空间的方向才会对最终积分有贡献,所以最后这个球面的积分就变成了一个半球的积分。

141905haa6uuj2hlkllh2h.jpg

蒙特卡洛积分

蒙特卡洛积分本身只是一种数值计算的方法,它和光线追踪本身无关:我们知道要求出某个函数的解析形式的积分是一件很难的事,有时候甚至是不可能的,因此,我们为了能够估计某个函数在一个定义域内的积分,就需要一些估计方法,蒙特卡洛积分就是这样一种方法。它的思路很简单:为了估计某个函数在一个定义域内的积分,我只需要在这个定义域内随机地找一些采样点,计算采样点所在位置的函数值,把所有采样点的函数值平均即可得到该积分的估计值:
155822dxi5i95lbrbg1xxl.png


很多人在学习编程之初可能都写过用抛针法计算圆周率的程序,仔细回忆,这个过程实际上用的就是蒙特卡洛积分。这个方法直观简单,对一些高维的难以求解的积分有非常好的效果,而渲染方程就是这样的一类积分,所以它常常被用在求解渲染方程上。

141909jmpt3um05z92p9a9.jpg
抛针法计算圆周率

那么如何采样呢?最简单的方法就是直接在积分的定义域内生成均匀分布的随机样本,但是实际上根据函数的形状不同,不同位置的采样点的函数值对最终积分结果的贡献也是不一样的。比如函数值较小变化又比较平缓的位置,生成太多的样本会浪费;而在一些函数值比较大变化又快的位置,可能生成的样本又太少。如果我们先验地知道函数的形状,那我们就可以针对性地生成非均匀分布的随机样本,这样能够在相同样本数量的情况下对目标积分得到一个更准确的估计,这就是我们常说的重要性采样。重要性采样和它的名字一样,就是尽量采样积分定义域内重要的点,少采样不重要的点。

141913tzuhwel03fbvdlte.jpg
重要性采样的示意图,说明了随机样本分布函数(pdf)选择的重要性,pdf的形状和被积函数越相似,则积分的估计值越接近真实值


改写我们之前原始的蒙特卡洛积分,得到下面的公式:

155915mr8vgs8ibbgzzvx9.png


有关如何重要性采样的更多内容可以看这个系列文章[6]。当我们得到了一个随机分布的概率密度函数(pdf)后,我们可以使用逆变换采样[7]的方法从一个均匀分布去生成任意概率密度分布的样本。

对于均匀分布的样本生成来说,有时候完全随机的样本反倒容易引入不必要的噪声,我们通常希望一个样本分布即有一定的随机性,同时总体来说又是有序地,能够很好地覆盖整个样本空间。这时我们就需要引入QMC[8](Quasi Mento Carlo)和低差异序列的概念了。关于低差异序列,我们可以认为它是一个确定的样本序列生成方法得到的一组样本。它看起来有一些随机性,同时又最大程度上均匀分布在了整个样本空间里。比较常用的两个低差异序列是Hammersley Sequence和Halton Sequence。前者需要知道总样本生成数量,适合确定样本数的低差异序列,后者则不需要,更适合步进渲染(Progressive Rendering)。有关低差异序列的详细介绍参见这篇文章[9]。

141915lwxuh3hufocxcexx.jpg
伪随机序列和低差异序列的区别

由于蒙特卡洛积分是一种随机采样的方法,因此它的估计结果也是一个随机变量,这个随机变量的统计特征就被我们用于衡量不同的渲染框架的效果,最常用的统计量是期望和方差(或者叫一阶矩和二阶矩),如果一个估计(随机变量)的期望和被估计量的真实值是一样的,我们就说这个估计是无偏的,如果一个估计的方差随着样本数的增加逐步减小(表示这个估计和真实值越来越接近),那么我们就说这个估计是一致的。样本数量和方差的关系可以用于衡量一个估计的收敛速度。有关这两个概念的解释可以看这个答案:文刀秋二:如何理解(un)biased render?

路径追踪(Path Tracing)

渲染方程本身给了我们一个很好的数学形式上的定义,让渲染不再是一个盲目尝试的过程,但渲染方程本身在绝大多数情况下是无法直接求解的,为此,人们把渲染方程用不同的数学等价形式改写,然后对新的方程形式进行近似求解。其中路径追踪就是这样的一种方法。它将渲染方程改写成了这样的形式:

160034allhjfw5t6vh40ts.png


其中,

160035ghnfqrz7ffh6ie2p.png


按照经典的渲染方程的形式,我们为了某个表面的点的颜色,首先要以它的法线为中心向半球内发射若干条光线,求出每条光线和场景的交点,要进一步以交点的法线为中心发射若干条光线,这是一个极难求解的递归过程。而路径追踪的方法把某个点的颜色看作是若干条光路(Path)合起来的贡献。一条光路可以认为是若干表面点连接而成的一条线段,为了计算某个点的着色,我们只需要以该点为起点,构建若干条路径,并将每条路径上的光照贡献叠加即可。

结合光线追踪的基本框架,我们可以认为路径追踪就是把光线以路径的形式重新组织了起来。

141917s52cckn52avceika.jpg
path tracing中的一条路径

DirectX Raytracing API

有关DXR的介绍主要是来自于官方文档[10],本文不打算介绍API的细节,只就整个Pipeline和一些基本概念做简单介绍,希望大家能够了解编写DXR程序的基本原理。由于目前DXR API只支持Volta架构的GPU,相信大部分同学都和我一样买不起Titan V,所以微软也提供了一套基于Compute Shader的DXR Fallback Layer实现。只要显卡能够支持DX12即可在其基础上进行编程。此外微软的PIX已经支持了DXR API的调试,但是如果使用的是Fallback Layer的话,看到的还是Compute Pipeline的一些调试数据。

回忆一下我们在DX12中使用Raster Pipeline时如何绘制场景:

(1)定义一些描述场景的数据,如几何数据,贴图,材质、灯光等信息,以Buffer和Texture的形式上传这些数据到VRAM;

(2)定义如何绘制模型的着色器(VS,PS,GS,HS,DS等);

(3)通过Root Signature定义Shader的形参,并使用Pipeline State Object(PSO)完成整个管线的配置(shader及一些固定管线的设定);

(4)通过各类数据的访问视图(CBV,UAV,SRV,RTV,DSV)规范化数据的访问形式,把其中的一些View绑定给shader作为它们的实参;

(5)调用DrawXXX的命令完成一次Draw Call。

DXR API的设计在最大程度上复用了DX12的已有框架,诸如Buffer,Texture,Command List等概念在DXR API中仍然有效,本篇并不打算单独对DX12进行详细的介绍,对DX12不熟悉的同学可以去看看微软的官方Sample或者这本书[11]。

着色器

在DXR API中一共有五类着色器:

Ray Generation Shader:这个Shader负责初始化光线,是整个DXR可编程管线的入口,我们在其中调用TraceRay函数向场景发射一条光线。Ray Generation Shader从结构上非常像Compute Shader(在GPU硬件中,它很可能也是通过Compute Pipeline来实现的),不同的是Compute Shader的组织形式是Group->Thread这样的架构,同一个Group中的Thread可以使用Shared Memory及一些原子操作进行数据共享和同步,但在Ray Generation Shader中,每条光线是彼此独立的,他们之间不需要线程间的同步和共享。此外RayGeneration Shader也负责最终着色结果的输出(通常是输出到一个UAV对应的Texture上)。

Intersection Shader:这个Shader是可选的,它只负责一件事,就是定义场景内基本的几何单元和光线的相交判定方法。如果场景的基本几何单元是三角面,则用户不需要自定义这个Shader,但如果不是三角面,而是用户自定义的几何形式,比如是用于体渲染的体素结构,或者是用于曲面细分的参数曲面,则用户需要提供提供相交判定的方法。这样的设定使得一些过程生成式模型(细分曲面,烟雾,粒子等)也能够使用光线追踪的框架进行渲染。需要注意的是,对于三角面来说,通过相交测试返回的是交点的重心坐标,用户需要根据重心坐标自行插值得到交点的相关几何数据(uv,normal等)。

Any Hit Shader:这个Shader的作用是验证某个交点是否有效,典型的应用是alpha test,比如我们找到了一个光线和场景的交点,但该位置刚好是草地中被alpha通道过滤掉的像素位置,我们希望光线穿过它继续查找新的交点,那么就可以在这个Shader中忽略找到的交点,此外,我们也可以在这个Shader中发射新的光线,一个可能的应用场景是折射/反射效果的实现。

Closest Hit Shader:当一条发射的光线经过和场景的若干次求交最终找到一个有效的最近交点后,就会进入这个Shader,这个Shader的作用很像我们在Graphics Pipeline中用的Pixel Shader,它用于某个找到的样本点的最终着色。

Miss Shader:这是一个可能会被忽略的细节,即如果找不到有效的交点怎么办?在真实世界中一条光线总是会和某个表面相交,但虚拟场景的有限空间内却并非如此,这时候我们可能希望进行一次cube map的采样,或者告诉Ray Generation Shader它没找到交点,这些行为都可以在Miss Shader里定义。

Shader之间如何传递参数?

就像Graphics Pipeline使用可变寄存器传递参数一样,Raytracing Pipeline使用用户自定义的payload数据结构传递参数。不同的是,可变寄存器的数据传递是一对多的,顶点处理阶段传递的参数要经过GPU的插值后传递给PS,但payload是伴随着光线在场景中传递的,一条光线只有一份,它伴随光线传递。每个阶段的shader都可以动态的读写这个数据结构,把信息传递到Raytrace Pipeline的下一个阶段。

场景数据结构

在Raytrace Pipeline中,几何数据由两层结构存储,其中底层的数据结构称之为Bottom Level Acceleration Structure,上层的数据结构叫做Top Level Acceleration Structure。

141920x9jearewe835r2yd.jpg

Bottom Level AS:这层存放一般意义上的顶点数据,类似于Raster Pipeline的Vertex/Index Buffer的集合。当然如果是非三角面的话,可能存放的是其他数据(比如参数曲面的控制点)。

Top Level AS:Top Level AS是模型级别的场景信息描述,它的每一项数据引用一个Bottom Level AS(当做该模型的几何数据),并单独定义了该模型的模型变换矩阵以及包围盒。

基于这样双层的数据结构,我们就可以调用DXR API创建整个场景的查询结构,这个查询结构是一个BVH(Bounding Volume Hierarchy),用来加速光线-场景的相交测试。可以想象,当一个相交测试开始时,光线首先会和BVH进行相交测试,通过的对象才会进一步访问其Bottom Level AS数据执行具体的光线-三角面相交测试。

由于整个BVH只用于进行光线-场景相交测试,因此它只包含顶点位置的信息,如果我们需要顶点位置之外的信息(uv,normal等),则往往需要额外自定义一个SRV/UAV Buffer用于存储这些数据。

着色器数据绑定

类似Raster Pipeline,Raytrace Pipeline也通过Root Signature去绑定shader所需的参数,不同的是,Raytrace Pipeline的Root Signature分为Global和Local,由于Raster Pipeline在场景绘制时是per object的,所以object相关数据和全局数据是没有区别的,但是Raytrace Pipeline是per ray的,为了传递per object的数据(模型用的贴图,矩阵等),我们必须再定义一系列的Local Root Signature。Global Root Signature和Raster Pipeline中一样,用于传递一些与具体模型无关的数据(比如整个场景的灯光列表,摄像机矩阵等)。

以上只是设定了shader的形参,在实参的传递时,Global Root Signature和Raster Pipeline里面一样,而Local Root Signature的实参传递则需要一个叫做Shader Table的数据结构(本质上是一个SRV Buffer)。这个Shader Table是一个数组,数组中的每一项被称之为一条Shader Record,每条Shader Record存储一个shader的id(固定长度)以及它对应的Local Root Signature的实参列表(变长)。这样,当某个shader操作具体的模型时,就可以根据它的object id去访问Shader Table中的某一条Shader Record来获取对应的局部实参。

管线状态对象(Pipeline State Object)

类似于Raster Pipeline,Raytrace Pipeline也有它自己的PSO,其中定义了会用到的Shader和一些基本的光线参数(光线最大递归层数等)。

绘制命令

在所有数据都准备完成后,我们可以调用DispatchRays发起一次光线追踪。

Raytrace Pipeline的流程

下图是Raytrace Pipeline的基本流程图,其中绿色的部分表示可编程单元,灰色的则是固定单元。可以看出Raytrace Pipeline以可编程管线为主,只有极少的固定单元。基本概念中我们已经介绍过,任何一个光线追踪的渲染程序都是从Ray Generation Shader开始,它负责初始的光线生成,生成的初始光线会通过固定的软/硬件单元对整个场景(我们构建好的BVH,在DXR中也就是Top Acceleration Structure)进行遍历求交,这个求交过程可以是用户自定义的一个Intersection Shader,也可以是默认的三角形相交测试。一旦相交测试通过,即得到了一个交点,这个交点将会被送给Any Hit Shader去验证其有效性,如果该交点有效,则它会和已经找到的最近交点去比较并更新当前光线的最近交点。当整个场景和当前光线找不到新的交点后,则根据是否已经找到一个最近交点去调用接下来的流程,若没有找到则调用Miss Shader,否则调用Closest Hit Shader进行最终的着色。

141924bqj3vko0qv2jq2te.jpg

我也尝试了这套API,渲染了一张满是噪点的图:

141926o0suddqzopoduqqn.jpg
图中的反射,AO及软阴影是Raytrace Pipeline生成的,其余部分则由Raster Pipeline及Compute Pipeline完成。

DXR API在实时渲染中的应用

混合已有的管线

尽管DXR看起来非常美好,也代表了图形渲染未来的方向,但是现阶段性能仍然是它最大的问题。因此,混合使用多种管线(Raster/Compute/Raytrace Pipeline)仍然是最经济有效的方案。同时这也能保证现有引擎的最少改动来支持光线追踪。文章[1]中的这幅图很好的说明了这个问题:

142936l5zhv9v9sgc5961c.jpg

可以看到,一个场景的绘制分为多个子任务,其中G-Buffer的绘制这类不需要全局信息的任务可以交给Raster Pipeline去做;而像是光照、后处理这类基于全屏绘制的任务,则可以交给Compute Pipeline去充分发挥;至于软阴影、反射、环境光遮蔽(AO)、半透明这类大量依赖全局光照信息的效果,则是Raytrace Pipeline的特长。此外,传统的光线追踪框架首先需要从摄像机向屏幕发射主光线并和场景求交,有了G-Buffer,我们可以直接读取G-Buffer的信息,这样就避免了主光线的生成。

有了这个思路,我们只需在一些全局照明的效果部分替换现有的技术模块(如SSR,VXGI,SSAO等)即可升级引擎支持光线追踪。

AO

AO的部分比较简单,一个公式就可以说清楚:

142936p1du1zeo0uhhhhhi.jpg

简单的说,就是以主光线的交点位置为起点,交点的法线为中心,发射若干条随机光线做半球上的蒙特卡洛积分,积分结果再除以PI归一化到[0,1]即可。积分函数也比较简单,就是某个方向上的可见性(Visibility)乘以采样方向和法线的点积,根据这个积分函数形式,我们可以选择做cos weighted的重要性采样。至于去噪的方法,文章[1][2]提到的都是直接用TAA。

我们知道,SSAO的方法为了找到半球内的可见性,一般是在GBuffer的某个像素为中心取一系列相邻像素作为积分的采样点,这是假设相邻像素在位置上离得也比较近,缺点是几乎只能找到比较近的遮蔽,稍微远一点的遮挡关系就找不到了,这两张图很好的说明了问题:

141932qq083p32q37huwvr.jpg

141935yn7uymalmcidmdyp.jpg

软阴影

软阴影的实现[1][2]也基本类似。我们知道,精确光源(点光源,方向光源)无论如何是无法生成软阴影的,因为半影区的形成就是因为光源本身有一定的体积。为了让这类光源产生软阴影,我们可以假设光源本身是一个球形,那么这个球形对于当前计算阴影的点张的立体角就是一个锥形区域,我们只需要以着色点为起点,在锥形区域内随机选择一个方向,用这条光线去做可见性测试。对于点光源,我们可以设定它的球形半径,这样立体角就是一个随着相对位置变化的值;对于方向光,我们认为它是一个无限远的球形,它对应的立体角是一个固定值。可以想象,如果我们将这个立体角逐步减少到0时,得到的就是一个精确的硬阴影。

141936bw6sag00aa67f6nr.jpg
基于cascade shadow map的方法和基于uniform cone sampling的光线追踪生成的阴影对比,视觉上后者更符合直觉。

间接反射

反射这部分[1][2]中的实现看起来是有比较大的差异,但总体来说思路都是从Screen Space Reflection这类技术扩展而来,有关SSR的各类方法我会在今后的文章中专门去综述。

[1]中沿用了自家Frostbite引擎的在SIGGRAPH 2015上用到的方法[12],好处是能够物理正确地处理粗糙度相关的反射(而不是简单的镜面反射)。具体实现时使用半分辨率的UAV Texture,把原方法中基于depth buffer的ray marching改为了直接用raytrace去找反射的交点,计算反射点的颜色,然后记录对应采样方向的概率密度函数(pdf),最后用一个resolve pass在全分辨率去做合成。关于这部分的实现,作者很贴心的在slides里面提供了伪代码,想要自己实现的同学可以去看代码。

141937pkfeokkbexxj333w.jpg
一张对比图,比SSR的方法最大的好处是反射点测量精确,不被屏幕空间内这一条件限制,着色方法则是和SSR类似

间接漫射

这部分两篇文章实现的思路差异比较大,[1]的思路是完全依赖实时的光线追踪,方法和AO的计算方法类似,也可以用cos weighted的重要性采样,只是被积分的函数换成相应的BRDF函数即可。但是不同于AO的是,因为要积分的不是简单的可见性,所以要得到一个视觉上较好的漫射结果,需要大量的光线样本。为此,[1]使用了一个point based的方法,这个思路就比较像我们传统的irradiance volume的方法,不同的是,irradiance volume的probe是设定在三维空间里,并且是离线烘焙的,而[1]中的每个point是分布在模型表面的,并且是实时(步进式)计算的,由于point的分辨率远低于屏幕分辨率,所以可以多使用一些样本去计算该点的indrect diffuse,计算最终颜色时,只需要根据像素的位置基于point去插值即可(这个和irradiance volume差不多)。文章中没有提到如何去分布这些point,但感觉这个分布的算法似乎可以用来生成草间弥生式的风格化渲染。。。

141938gh9tcirx3n666sih.jpg
point based方法的可视化,下面的图让我想到了草间弥生,好像直接用这个结果也很好看呢。

[2]的方法相对来说没那么激进,是一个混合irradiance volume和raytrace的方案,它的思路也不复杂:对着色位置,基于重要性采样在半球发射一系列短光线(这个很重要,因为短光线可以很大程度上加速单根光线的raytrace,这个优化对于AO也适用),如果短光线有击中某个临近表面,那么我们就基于位置对那个表面位置进行一般性的irradiance的计算,如果短光线没有击中附近表面,我们就改用光线的终点位置的irradiance volume的信息。这样,对远处的静态物体,大部分贡献来自于irradiance volume技术,而近处的动态物体,则是依赖于实时光线追踪的结果。下面是这个方法的示意图:

141939wq06b80bawkq88av.jpg
没有hit到的光线用irradiance volume的数据

141940w6e0f3xc09l9b6ll.jpg
hit到的光线用光线追踪的结果

这个方法相对于传统irradiance volume的方法有两个好处,一是可以用于动态物体,而是减少了irradiance volume的漏光问题。

玻璃和SSS材质

[1]提到了他们的玻璃和SSS材质的做法,玻璃这部分比较简单,大致就是在两种材质交界的位置根据折射率生成对应方向的折射光线,然后根据材质本身的吸收率对光线进行吸收即可。需要注意的是,如果是毛玻璃,只需要在材质交界处的锥形区域内随机发射若干折射光线即可。

141941z5w3dc59d3d3m8e0.jpg
光滑玻璃和毛玻璃在折射光线生成时的区别

SSS材质的实现比较有意思,它用了Frostbite在GDC 2011上提出的一个老技术[13]。具体做法是,对每一个着色表面,将顶点位置沿着法线的反方向推一小段距离到模型内部,以该点为起点,向整个球面发射光线做光线追踪,针对每一个光线追踪的交点(仍然是模型表面的点),利用比尔定律和各向异性的HG phase function计算它相对于模型内部点的单散射,最后gather所有交点的计算结果作为原表面点的BTDF。做过体积云、体积雾的同学对这些概念一定不陌生。有关散射、体渲染的内容我们今后会有专门的文章去综述,这里不做展开。

141941cn0awac6j663zaww.jpg

141942ednrdao1jrd31nrc.jpg

141942r5llsz7slll8qsvi.jpg
计算BTDF的基本流程

141943vaayzvg3haeep5oc.jpg

纹理过滤

纹理过滤其实是一个很容易被忽略但又很重要的问题。我们知道纹理贴图是一个多级的结构,也就是我们常说的mip-map,有了mip-map,在对纹理进行采样时,我们就可以选择一层合适的mip-map。但有许多人不了解mip-map的原理:当我们从屏幕出发渲染一个场景是,屏幕上的每个像素实际上不是一个点,而是对应场景模型上的一个小的表面,我们一般叫做footprint,这个表面上包含的纹素(texel)不止一个,如果我们只采样单个纹素就会造成shading alias,为此我们构建mip-map。mip-map的下一层是上一层分辨率的一半,并且是由上一层经过卷积运算得到的。这样就可以根据屏幕像素对应的纹理空间的footprint大小去选择一个层合适的mip-map去采样。我们可以使用u,v坐标对于屏幕空间x,y方向的偏导数形成的向量的叉积去估计footprint的大小。footprint面积和面积到mip-map level的映射公式如下:

141944jnrhlndoiiridl7i.jpg

接下来的问题就是如何计算uv相对屏幕空间xy轴的偏导数,在光栅化管线中,这是自动获得的。因为pixel shader会以32/64个像素(N卡和A卡及各代架构会有所不同)打包为一组去执行(在vertex shader中则是32/64个顶点打包为一组),这一组像素称为一个warp,而一个warp内,又会以2*2的像素块组织,这一个像素块称之为一个pixel quad,在像素块内的四个像素在执行PS时是完全同步的(实际上warp内的所有像素也都是完全同步的),也就是说当某个像素执行到某一行时,其他像素也必须执行到了同一行。有了这个特性,我们就可以使用pixel quad相邻像素的uv值做差分得到uv的偏导数,也因此我们可以使用ddx,ddy的命令去求取任意一个变量的差分。

到了光线追踪管线中,我们不再有pixel quad这样的概念,为了获取uv的偏导数,就只能给原始光线添加两根辅助光线用来计算偏导数,这两根辅助光线会在原始光线的基础上向上/右偏移一个像素的距离。然后两根光线独立做光线相交,交点的uv和原始光线交点的uv做差分去获得偏导数。

141945vodw5g45ai97zoih.jpg

这是传统光线追踪渲染器解决纹理过滤的方案,但是它使得我们需要发射的光线变成了原来的三倍,是比较低效的。这里EA和NVIDIA合作开发了一个新的纹理过滤方案,但是怎么做没提,要等NVIDIA放出相关的论文实现了。

降噪

这个部分也没有太多描述,可以总结为一句话:TAA大法好。[1]借鉴了SVGF的方法[14],在TAA的基础上再做空域的滤波,[2]基本只用了原有的TAA框架。

结尾

有关本文介绍的光线追踪的一些基本概念,建议大家去看Scratchapixel有关光线追踪和蒙特卡洛积分的系列入门文章[6][15]。

进一步去了解全局照明算法相关的知识,大家也可以去读经典的PBRT:《Physically Based Rendering:From Theory to Implementation》,秦春林老师写的《全局光照技术》也是非常好的相关书籍。

需要进一步了解DXR API,建议去看微软的官方Sample:Microsoft/DirectX-Graphics-Samples,此外知乎上也有两篇这个主题的文章[16][17]。

另外明年会出版《Ray Tracing Gems》,应该会有大量的工业界如何使用实时光线追踪的例子,大家可以关注。

嗯。。。所以说人生就好像一个蒙特卡洛积分,你的世界观就是那个积分值,而世界观的形成就是在以你自身为中心对这个世界进行估计,估计的方法就是去经历各种事,每一件经历过的事给你的内心体验最后都会作为一个样本更新你的世界观。随着时间的推移,你经历的事越来越多,世界观也就越来越稳定,几乎没什么事再让你兴奋了,你就变成了一个无趣的成年人。接受教育就像是重要性采样的过程,它能保证有些事即使你没有经历过,也能通过已有的知识去推测和判断,但同时也很可能让你更快地变得无趣。。。有关渲染的话题还有几篇内容在写,希望今年草稿箱可以清空。另外最近初来杭州,喜欢图形的朋友(特别是网易的各位大佬)可以考虑找我面基。也欢迎喜欢做游戏(尤其是galgame)的小伙伴一起聊聊或者做些有意思的东西。

PS:今天在Siggraph 2018上我的前老板(还好去年年会的时候完成了和黄老板合影打卡的新手任务)发布了新一代图灵架构显卡,也是首款硬件支持光线追踪的GPU,感兴趣的同学可以关注新显卡发布的一些消息:NVIDIA Unveils Quadro RTX,World’s First Ray-Tracing GPU,重点可以关注GigaRays/sec这个光线追踪的指标,大家可以自己换算一下,大概相当于多少spp per frame。另外,接下来可能会考虑再写一篇有关光线追踪实时去噪技术的综述,作为本篇文章的进阶内容。

引用


[1]Shiny Pixels and Beyond:Real-Time Raytracing at SEED
[2]Experiments with DirectX Raytracing in Remedy's Northlight Engine
[3]Rendering equation
[4]Real-Time Rendering,Third Edition.8.1 Radiometry for Arbitrary Lighting
[5]Introduction to Light,Color and Color Space
[6]Monte Carlo Methods in Practice
[7]Inverse transform sampling
[8]Quasi-Monte Carlo method
[9]低差异序列(一)-常见序列的定义及性质
[10]DXR Functional Spec
[11]Introduction to 3D Game Programming with DirectX 12
[12]Stochastic Screen-Space Reflections
[13]Approximating Translucency for a Fast,Cheap and Convincing Subsurface Scattering Look
[14]Spatiotemporal Variance-Guided Filtering:Real-Time Reconstruction for Path-Traced Global Illumination
[15]Mathematical Foundations of Monte Carlo Methods
[16]光线追踪与实时渲染的未来
[17]DirectX Raytracing(DXR)functional Spec阅读笔记+注解

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

QQ|手机版|小黑屋|九艺游戏动画论坛 ( 津ICP备2022000452号-1 )

GMT+8, 2024-3-29 17:47 , Processed in 0.119165 second(s), 29 queries .

Powered by Discuz! X3.4  © 2001-2017 Discuz Team.