详细理清各类光线追踪原理
文章目录
前言
全文采用通俗语言,不罗列数学知识和专业知识,仅分享自己的心得。
本文为什么说是“各类光线追踪”的原理呢?是因为我当时初识光线追踪的时候,从很多博客或者国内外论文上,都会看到光线追踪原理的介绍,但是经常会存在或多或少的差别,对于刚接触光线追踪渲染的人来说很容易被搞懵。经过研究生阶段我一直在接触学习光线追踪,所以对光线追踪不敢说有很深入的认识,但是也有一点小小的心得,因此想将光线追踪的原理分门别类地说清楚。
一、关于光线追踪原理的说明
光线追踪的实现原理,不是像我们平时课程学习时遇到TCP三次握手的原理那样有固定的样子,而是有点类似我们学习快速排序算法时那样有多种实现,快速排序算有很多种方法实现,核心都离不开找基准数然后遍历数组交换元素这一条(如果你不清楚TCP三次握手和快速排序是什么,可以完全不需要理会,仅仅只是帮助思考的比喻)。
开始正题,对于光线追踪,请首先放下你之前对其印象中的原理,看看下一段内容重新去认识光线追踪这个词(如果你有深入学习光线追踪并且自己已经有系统的认识,也可以参考下我的内容,如果有错漏还请提供您的宝贵意见)。
光线追踪: 是一个框架,是一个统称,没有统一的实现原理,根据不同的使用场景可以有各种具体的实现原理。但是万变不离其宗,都离不开“通过模拟光线的现实传播行为来追踪光线进行渲染”这一个核心,只要根据这个核心来实现的渲染方法,都可以归到光线追踪下,都可以说是光线追踪渲染方法。因此你在不同地方看到的光线追踪原理会有差异就是这个原因。
二、关于光线追踪的分类
光线追踪主要分为以下几种具体的实现原理,建议通过英文全称来区分:
光线投射(Ray Casting):严格意义上这种不算光线追踪,但也是不得不提到的。
经典光线追踪(Classic Ray Tracing或者Classical Ray Tracing):也会直接被简称 “光线追踪(Ray Tracing)” 这应该是我们直接百度时看到最多的那种。这种是基础、传统、古早一点的光线追踪。
递归式光线追踪(Recursive Ray Tracing或者Whitted-style Ray Tracing):也叫做Whitted光线追踪,指同一个东西,忘了的话容易以为是两种不同的光线追踪。这种目前更偏向主流的光线追踪,可以说当我们谈论到光线追踪时,实际上就是在谈论这种递归式光线追踪。
路径追踪(Path Tracing):你有接触的话,可能会见过一种说法是 “路径追踪=光线追踪+蒙特卡洛” ,其实远不止如此简单,这种方式我也是接触最少的一种,因为它的实时性约等于无,关于光线追踪的实时性这点后面我会详细介绍。在渲染画面效果上(或者玩游戏爱说的光影效果),路径追踪是以上几种光线追踪中最强的。
以上几种就是光线追踪这个框架下最主要的分类,不把光线投射算在内的话,就是一共三类。下文将用通俗语言详细介绍这几类光线追踪的原理和我自己的心得理解。
三、关于光线追踪的核心
上文提到各种光线追踪“都离不开“通过模拟光线的现实传播行为来追踪光线进行渲染”这一个核心”。那怎么去解释这个核心呢?
我们人眼能看到外面的世界,能看到物体,看到颜色,看到光,是因为我们的眼睛接收了光,来自光源直射的光,来自物体发射的光。而那部分没有进入到我们眼睛的光,或者说没有被我们眼睛接收的光,那错过就错过了,为我们人眼成像提供不了任何帮助。光线追踪就是基于以上阐述提出的一个想法,通过追踪进入我们人眼的那些光线,看看这些光线从光源到我们人眼的途中发生了什么传播行为,例如经过哪几次反射,甚至折射,计算这些光线对我们眼睛成像的贡献(例如这些光线有多亮,是什么颜色的)。而以上过程的核心就是光线是怎么传播的。
那这个核心从算法实现上怎么去完成呢?
光路是可逆的,一条光线从光源发射进入我们的眼睛,这条光路反过来看就是从我们的眼睛发射到碰撞光源,虽然光路逆转了,但是路子是一样的,所以这条光路对我们成像而言没有受到变动影响。从算法上看,如果将眼睛作为光线的发射点的话,那意味着我们追踪的所有光线都是上面说的“能进入我们人眼的”那部分提供成像贡献的光线,从而不需要考虑那大部分没有进入我们人眼的光线,因此对算法而言可以避免更大的计算量。因此光线追踪都是指从我们人眼发射光线,追踪这些光线寻找光源的过程。
而我们人眼在计算机三维空间中可以视作一个坐标点,我们的视网膜可以看作是分辨率800x600的一个网,比喻我们看到的内容由800x600个像素点组成,我们在代码中用缓存来保存起来,例如使用二维数组,另外我们也要记录这个网的三维空间位置。我们将人眼坐标点作为起点,将这个网的其中一个像素的坐标点作为方向,连接起来的一个三维向量就视作一条光线,这条光线进入到场景中便可以说是发射光线,这条光线的贡献值最终计算为它一开始所经过像素点的颜色值,颜色值存入像素缓存区中,当缓存中所有像素点都处理结束,意味着成像结束。可以发现,计算机中实现光线追踪,就是一个三维几何计算的过程,处理的都是三维向量的计算。
四、光线追踪分类
(1)光线投射(Ray Casting)
有的文章会说光线投射是早期的光线追踪技术,属于光线追踪的一种,或者也有说光线追踪是由光线投射发展而来。我没有刻意去追本溯源,但是在我看来,两者有很大的相似性,也有很明显的区别。先介绍光线投射是怎样的:
先说光线投射用来做什么的,光线投射已经应用了很多年,也有很成熟的实现方法,最主要的用处在“体渲染”(volume rendering)上,体渲染也叫体积渲染,我们常听说的科学可视化中,利用CT图像进行三维重建的这项任务,就是借助体渲染完成的,渲染效果如下图所示:
左图来源网上,是医学中用到体渲染这项技术渲染出来的显示效果。体渲染区别于面渲染(表面渲染),渲染结果我们可以看到渲染对象内部的样子;而面渲染顾名思义就是只对物体表面做渲染,渲染效果就像我们现实生活中见到的物体一样,只能看到物体的表面,看不到内部。
右图就是光线投射进行体渲染的一个简单示例,因为体积就是一个正方体。那么为什么体渲染能渲染出物体的内部呢?其实实现这一点所依赖的就是输入的数据——我们平时接触比较多的三维模型(OBJ,STL格式),包含的仅有模型表面的数据,例如顶点坐标,组成三角形的顶点索引,可以理解为是空心的,而体数据会包含体积内部的数据,可以理解为是实心的。
那么拿到体数据,光线投射是怎么做的呢?
请看上面原理示意图:从人眼(或者摄影机)穿过分辨率网(屏幕)中的像素点,发射光线进入三维场景中,这条光线将一直延伸到给定的终点,光线中途会穿透一切东西,所以光路是直的。然后对这条光路以固定步长来逐个选取采样点,看看采样点有没有穿过体数据,有的话就根据对周围的体数据采样结果来计算贡献值(例如该采样点处的颜色和透明度等,通过插值计算),接着将所有采样点的贡献值汇总叠加在一起作为这条光路的结果值,也就是对应像素的颜色值。每个像素都这么处理完就意味着渲染完毕。
上面的过程中,只有最初时投射出光线进入场景,而没有光线在场景中反射,也就是更像我们做X光那样只有光线的直线穿透,没有模拟光线的物理传播行为,因此严格地也说不上是光线追踪渲染。其实与光线追踪在原理上有一个很大的相同点就是都会通过从眼睛向场景发射光线来进行渲染。
(2)经典光线追踪(Classic Ray Tracing)
前文也说过,我们直接百度搜索光线追踪的话,最经常见到的介绍或者原理图,就是这种光线追踪,而经典光线追踪和递归光线追踪这两种也是最容易让刚学习的人搞乱原理,所以要搞清楚光线追踪,很重要的一点就是要清晰地分清楚这两种。
先说经典光线追踪会用来做什么的,用得最多的地方在科学可视化工具领域中,我比较熟悉的是分子可视化工具,工具中用到的光线追踪渲染几乎就是经典光线追踪,而且很早就开始支持光线追踪渲染,并非近几年才开始支持,当然其他领域的可视化工具也是大多使用经典光线追踪。另外在科学可视化领域中如果要渲染出高质量的出版物图像的话(例如发表文章中的配图),也会使用到经典光线追踪。
大家看看上图,尝试判断一下,那一张图是经典光线追踪渲染出来的图像?
答案是两张都是来自经典光线追踪渲染得到的。你可能会很奇怪为什么两张看起来那么假,跟现在游戏里支持的光线追踪(例如RTX ON的演示视频)差太远,效果这么llow的真的来自光线追踪吗?其实觉得真实感渲染效果的主要在渲染方程上(即每个像素点颜色的计算公式)
像上面右图球体与球体之间的阴影,这么简单的阴影,光栅渲染也能做,光线追踪也能做,可以得到效果一模一样的图片也很简单,但是他们的计算的方法不一样,渲染方程也不一定一样。
那就说说经典光线追踪是怎么做的呢?我就截取百度最常见到的光线追踪原理图来介绍:
这张图可谓是烂大街了,因为这张图是具有代表性的,但是注意,这张图仅仅说明了经典光线追踪的原理。
同样是从人眼(或摄像机)发射一条光线,穿过屏幕上的一个像素点,进入三维场景中。我们需要判断这条光线是否与场景中的物体发生了碰撞,要先知道大多数情况下,场景中的三维物体是由众多三角形面片组合而成的,可以视为场景中实际上有一大堆三角面片,判断光线是否与场景中的物体碰撞,实际上是计算光线与三角形相交,找出离眼睛最近的那个相交的三角形。用最土的方法就是访问每一个三角形面片,计算光线是否跟它相交,现代的方法就是借助空间上的加速结构(例如BVH和KD树),快速找到相交的三角形。
那意味着有两种情况:(1)光线没有射中任何东西(在上面的原理图中没有体现这种情况),那射空了就给相应的像素点填上你给定的背景色。(2)光线射中了物体,那么我们追踪光线。这里正是区别经典光线追踪和递归光线追踪的主要地方。
那光线射中了物体后,经典光线追踪是怎么做的?
现实中光线击中物体后,会发生折射与反射,光线会继续朝新的方向前进。而经典光线追踪,光线也会计算击中物体后的反射方向(很少用到折射),通常计算的是镜面反射,得到反射方向后,光线不会继续朝新方向前进(经典光线追踪最重要的特征之一),意味着光线发生首次碰撞后便会就此打住,不会继续前进。那么计算的反射方向是用来干嘛用呢?其实用来计算像素颜色值的,不是用来作为新的前进方向的。
光线首次击中物体,那么就能得到碰撞点,然后将该点与光源连一条线(简单的是用点光源),判断这条线能不能直达光源,也就是计算这条线有没有与其他物体发生相交,如果有相交,则说明光源射到碰撞点的光被遮挡了,那么就按照渲染方程去计算阴影下这个像素点的颜色值;如果没有发生相交,则说明光源能直接照射到碰撞点,那就计算光照下的像素点颜色。之前计算得到的光线反射方向,用来计算光源与碰撞点之间的连线在反射方向上的投影(利用向量点乘计算),理解为光源对这条光线的贡献值。
对应阴影的计算,狠一点的可以直接给黑色值,缓和的话可以对颜色值乘一个衰减因子,都是自己设计的计算公式而已。
对每个像素点都这样子发射光线进行计算,最后所有像素点颜色都算出来就是一张简单的经典光线追踪渲染图了。经典光线追踪就是基本这样,可以再回看刚刚的原理图,是不是容易理解了。
(3)递归式光线追踪(Whitted-style Ray Tracing)
Whitted光线追踪跟递归式光线追踪是一个东西,由Whitted这位学者在80年提出,因为算法是用到递归的,所以跟经典光线追踪(看上面的介绍明显没有用到递归)区分开,也被叫做了递归式光线追踪。
先看看递归光线追踪渲染出来的效果:
是不是第一眼发现图片中的小球像玻璃球那样,表面能够反射环境中周围的球体包括地板。是不是感觉真实感增强了很多。
这种效果已经很像是我们目前游戏光追演示视频中开启光线追踪的效果了,也就是网友调侃的开光追必下雨,地面肯定积水,光追场景必出现玻璃。
因为这种类似镜面反射的效果,是最能看出光栅渲染和光线追踪渲染区别的地方。如果上图中的球体是磨砂材质的话,那么光栅渲染和光线追踪渲染出来的画面其实差别不远。
我们就从镜面反射这一点入手,说说为什么这种效果最能看出两种渲染的区别。我们回想一下上面介绍的经典光线追踪,思考一下,如果用经典光线追踪来计算上图这种表面能看到其他球体的类似玻璃球的效果,要怎么做?
答案是做不了,因为你要在玻璃球表面看到其他球体,说明你的眼睛要接受从其他球体反射过来并进入你眼睛的光线,而经典光线追踪中的眼睛只会接受来自光源反射过来的光线,这种信息缺少造成根本没法用经典光线追踪的方法去计算这种效果。
那思考一下用光栅渲染去实现这种效果,可以做到吗?
答案是很难,同样是必要信息的缺失(或者说很复杂才能获取得到)造成了没法去计算真实的这种效果,那么光栅渲染一般是怎么去模拟这种效果的呢?——常用的比较方便且取巧的方法是用贴图,相当于欺瞒地模拟这种反射效果。但是,归根到底也是假的。——所以递归式光线追踪的优势呼之欲出了,就是能够获得更丰富的信息来模拟计算真实的光照和阴影效果。一个是用贴图模拟出来的,一个是利用光线物理行为实实在在计算出来的,哪个结果更具真实感也就很明显了。
所以演示光线追踪的视频中经常能见到大厦的镜面窗户,下雨后的地面积水,这种类镜面反射的效果最能凸显光线追踪在发挥作用。
下面就来介绍为什么递归光线追踪能获取得到更丰富的信息,也就是递归光线追踪是怎么样做的:
上图是递归光线追踪原理示意图,一眼就能看出和经典光线追踪原理图的区别——光线的路途变得更远了,光线也会在场景中的球体之间发生反射了。也就是说光线在场景中会发生多次碰撞,这一点正是递归光线追踪和经典光线追踪的区别处。
同样是从人眼出发发射光线,穿过屏幕进入场景中,寻找光线最近的碰撞点,这开头都和经典光线追踪一模一样。关键在找到光线的首次碰撞点后,经典光线追踪是直接利用这个点和光源一起计算像素颜色值。而递归光线追踪则是将这个碰撞点作为光线的新起点,再在这个点上计算光线的反射方向作为光线的新方向,然后继续追踪这条光线,即继续寻找这条新光线的最近碰撞点(从算法实现上,这一步可以用递归函数去实现)。
接下来,光线遇到新的碰撞点,就生成新的反射光线继续追踪,连起来看就是一条光路,而我们就是在追踪这条光路,那么这条光路最终的结局无非就是两种:
(1) 光线命中光源(这时光源可以是面光源、点光源,为了提高命中率,通常选面光源),利用这条光线沿途得到的信息来计算相应像素点的颜色值,沿途的信息是指中途的碰撞点(可以知道这个碰撞点是在哪个球上,这个球是什么颜色的等等),将各个碰撞点上的信息套入渲染方程计算,其实就是将各个碰撞点对光线做出的贡献进行混合。例如上图中A1是红色的,A2是蓝色的,A3是绿色的,光线每碰撞一次都会乘一个衰减系数(如0.8),用来减弱后续碰撞点贡献的权重,将各个点的颜色等信息带上衰减系数一起套入渲染方程里搅浑、混合、整合一下,结果值就是我们眼睛接受到这条光线的贡献值,即相应像素点的颜色值。(从结果上看,我们现在就能从玻璃球的表面看到反射的其他玻璃球了,因为像素颜色中有其他玻璃球上碰撞点的贡献)
(2) 第二个结局就是光线没有命中光线,就是光线最后没有与任何三角形面片发生相交。同样的可以像经典光线追踪一样,把这个像素点设定为黑色,或者背景颜色。在游戏场景中就是天空盒。
那么如果我们当前场景比较密闭呢,一条光线一直在场景中不断反射,就是没有击中光源的话,那怎么办?
从算法实现上看,就是光线一直在递归,没有达到上面两种结局,没有停止递归的条件。——那我们就人为给定递归终止的条件:当光线碰撞了50次还没有击中光源或者射空(即递归50层),则终止递归,按照射空去处理。
如果只是想了解递归光线追踪的原理的话,看到这里就已经了解完了,如果有兴趣了解更多这些技术细节的话,可以继续看下面的介绍。
随之而来出现一个算得上是误差的问题:假如光源是一个只有芝麻点大小的点光源,那是不是可想而知会出现大量没有击中光源的光线,这些光线对应的像素点如果设定为黑色或者白色背景色的话,那就会出现类似下图这种效果:
出来的图片存在大量噪点,失真。这种通常视为欠采样。
即使增大增多光源也还是会出现这种问题,最基本有效的解决方法就是多次采样。
我们看看我们目前介绍到的递归光线追踪的算法框架是怎么样的:
color = (0, 0, 0); //初始化颜色为RGB值均为0
for(int x = 0; x < width; x++) //图像的宽度
{
for(int y = 0; y < heigth; y++) //图像的高度
{
color += RayTracing(x, y); //光线追踪入口,是一个递归函数(简化了参数)
}
}
我们目前只对每个像素点发射一条光线,也就是只做一次采样,算法可以看成:
color = (0, 0, 0);
for(int sample = 0; sample < 1; sample++) //采样数量 = 1
{
for(int x = 0; x < width; x++)
{
for(int y = 0; y < heigth; y++)
{
color += RayTracing(x, y);
}
}
}
那如果我们做多次采样,同一条光线每次采样都是穿过(x, y)像素点的话,那么光线是一模一样的,后面碰撞得到的光路也是一模一样的,输入同样的数据只会计算得到相同的颜色。所以为了使得每次采样的结果出现细微差异,会对(x, y)像素点,在每次采样的时候进行一次 一个像素 范围内的偏移,以此稍微改变每条光线的发射方向。这样做的效果就是对一个像素点来说,部分光线击中光源,部分光线射空,对这些光线的结果求平均值作为结果,便不再是要么有色,要么黑色,从此颜色会更缓和,例如物体边缘部分会有颜色的渐变——这也是同时带来了反走样(抗锯齿)的效果。
那么算法框架变成:
color = (0, 0, 0);
for(int sample = 0; sample < 20; sample++) //采样数量 = 20
{
for(int x = 0; x < width; x++)
{
for(int y = 0; y < heigth; y++)
{
double offset_x = random(-1,1); //生成一个-1到1之间的值作为x的偏移值
double offset_y = random(-1,1); //生成一个-1到1之间的值作为y的偏移值
color = color + RayTracing(x+offset_x, y+offset_y);
}
}
}
color = color / 20; //采样总值求平均值
效果如下图:
噪点大大减少,真实感大大增强——但是计算量也大大增加,采样数为1的时候可以做到1秒渲染100张图片,相当于100帧每秒。采样数为20的话,缺只能1秒渲染渲染5张,帧数马上下降为5帧每秒,而且虽然图像质量更好了,但是放大细看还是不经看,其实噪点依然不少。
再看回本节开始的那张玻璃球的图像,是不是觉得这么高质量可能需要最少五六十次的采样?其实不是的,因为增加采样数只是一种最基本的降噪方法而已,还有很多降噪方法可以更高效地完成这项工作,例如前沿的会直接用上深度学习降噪。
(4)路径追踪(Path Tracing)
本身能把光线追踪讲清楚的中文资料不多,再把路径追踪加进去讲,真是越看越糊涂。我经常见到的一个说法是:
路径追踪 = 光线追踪 + 蒙特卡洛积分
想用一句话去概况路径追踪,但是这个说法是不准确的。我先不解释为什么不准确,我先讲清楚什么是路径追踪。
路径追踪与递归光线追踪都是按照递归的方式去追踪光线的。我们不需要非得去找两者的区别,实际上路径追踪是在真实模拟光路传播这件事上做得更深入一点。你应该理解为在递归光线追踪这个思路下,寻求了更进一步去贴近真实光路传播的方法,并命名为路径追踪。
那么递归光线追踪已经很好的追踪了整条光路了,那么还能怎么更进一步呢?
那就是在光路传播的准确性或者说仿真度上做工作,仿真度是指在玻璃和在布料上,光线的反射模式是不一样的。在递归光线追踪中,我们会考虑这种材质上的区分,例如光线的反射计算的区别。但是不考虑其仿真度。那么我们现在要开始考虑提高这个仿真度了。
我们引入材质(或者说材料),我们用材质去提高这个仿真度,我们利用材质告诉光线应该怎么样反射。这里所说的材质跟我们平时接触的Texture纹理贴图是不同的。这里说的材质会给我们提供两个关键信息,BRDF(双向反射分布函数)和PDF(概率密度函数)。
这两个词从字面意义上没法理解,我直接告诉你它们负责干嘛的,我们知道材质告诉光线应该怎么样反射的。那么:
-
PDF负责告诉你,光线击到这个材质时,朝各个方向反射的概率是多少。例如在玻璃材质上反射,那么光线大概率会朝着镜面反射的方向反射,很小概率会朝别的方向反射,如下图。PDF用数学公式描述了这个概率分布。
-
BRDF负责告诉你,击到材质的入射光所提供的光量,有多少会贡献到这条出射光线上。例如入射光线击到玻璃,会提供100个单位的光量。然后你只给一条的出射光线,如果这个光线是对准纯镜面反射方向的话,那么这个出射光能够得到接近100个单位的光量;但如果你的出射光线离纯镜面反射方向有点远的话,可能只能得到50个单位的光量,大意如下图。这个内容有点像上文提到的光衰减,但是BRDF更合理准确。而BRDF用数学公式描述了这个贡献量的计算。(你理解这个关于贡献量的概念即可,可忽略更深入的辐射度量学)
现在我知道了光线在哪个方向反射的概率,已经光在这个方向反射的话能够获得多少光量。那么很关键的一个问题是:这个出射光线怎么得到?
答案是随机采样,入射光击中材质表面,会在表面的半球区域内反射。在递归式光线追踪中,我们会有固定的计算方法去算出反射光线。但是现在要更深入地模拟光路传播,真实光线一定会朝着这个方向反射吗?显然不是,正如将一个小球自由落地,小球每次反弹的方向都是不一样的。
最简单的肯定是均匀采样,就是我们暴力地处理,入射光线击中表面了,然后在半球区域内的哪个方向的反射概率都是一样的,即使这个表面材质是玻璃,这就是均匀采样,即使反射方向很偏也没关系,因为我们有BRDF把关,太偏离镜面反射的反射光就得不到什么光量贡献。当然考虑效率,我们会用重要性采样, 重要性采样就是带权重的,我一次随机采样的结果,会更可能出现在权重高的地方,以玻璃材质为例,我采样(随机生成一条出射光线)的结果更可能出现在靠近镜面反射的方向。
而在半球区域内随机生成一条出射光线,是基于刚刚的PDF去写的采样函数,是遵循PDF的概率分布的。
那现在我们在递归式光线追踪的流程加入刚刚的内容,梳理一下整个过程:
我们从人眼发射一条光线,光线击中了玻璃材质的物体,我们通过重要性采样方式的随机采样去生成一条反射光线,该光线符合PDF概率密度函数分布,即这条反射光线虽然是随机数生成的,但是通过采样函数的帮助,它更可能出现在镜面放射方向附近。我们通过BRDF函数计算这条反射光线能得到多少光量贡献,然后继续追踪这条反射光线,一直递归下去直到结束,例如到达光源。
这个过程就是路径追踪的过程。在递归式光线追踪的基础上,把反射光线从通过给定公式计算变成了通过更写实的重要性采样获得。把光的衰减从给定衰减因子变成了使用更科学的贡献量函数计算。整个过程变成了尽可能拟真的光路传播,不需要做额外的阴影探测。光路是怎么样,结果就是怎么样。
你看明白上述两个改进点,你就已经弄清楚了路径追踪。
而所谓的路径追踪 = 光线追踪 + 蒙特卡洛积分,你也能理解为啥说它不准确,概括得太离谱。
作为概念理解,至此你可以结束路径追踪的内容了。下面内容会再深入一点,感兴趣可以继续往下读。
我一直没有提到蒙特卡洛积分,为什么会出现上文这个说法,肯定哪里有用到蒙特卡洛积分这个东西对吧,让我们先回到刚刚我描述的路径追踪的过程,我埋了一个坑。你可以再细读一遍,在脑海里模拟一遍这个过程,里面有一个东西我没有提——光路追踪完了,怎么计算像素点颜色?
你或许会想到用Blinn-Phong模型去计算像素点颜色。可是我们多花了这么多计算量,结果还是用到Blinn-Phong模型的话,那意义何在,而且我刚刚说的追踪过程也不符合Blinn-Phong的计算方法。
实际路径追踪用到的是著名的渲染方程:
先不纠结里面的符号,我们从整体去看,这公式是一个A + 一个积分B组成的。实际就是物体自发光A + 外部光影响B(半球区域积分)得到出射光结果。很好理解是吧,出射光量 = 自发光+外部光反射。
其中积分部分就是我们追踪光线的部分, f r f_r fr就是BRDF函数, L i L_i Li就是入射光量,它也是上一条光的出射光量,上一条光是由光源发出的,我们才能确定最初的入射光量。当然,我们光线追踪的时候这得反过来看(别忘了光的可逆性这一点)。
我们路径追踪计算像素颜色就是用到这一条公式,我们需要写成递归函数,去把这个公式用代码实现。
但是,积分用代码要怎么算?
这时才到蒙特卡洛积分登场,蒙特卡洛积分是用来计算上面积分的一个工具,通过将这个积分改为使用离散的计算去近似其结果值,看起来有点像用穷举法去近似这个积分。而结合了重要性采样后,能够避免穷举法的大量计算,又能够获得误差更小的近似值,最重要是能够用代码实现。想具体了解蒙特卡洛积分是怎么计算的,可以看我写的这篇通俗讲解清楚光线追踪中的蒙特卡洛积分。在这里你只要记住蒙特卡洛积分是用来计算积分的工具。有一点容易被误导的是,容易误认为蒙特卡洛积分是在生成随机采样的反射光线时参与,实际并不是这样的,蒙特卡洛积分只是用来算渲染方程中的积分,或者说随机采样反射光线本来就是用蒙特卡洛算积分的一部分内容(我们用离散的方式计算积分,那就要采离散点作为样本,而离散点指的就是随机采样得到的反射光线)。
而对于半球区域的积分,我们做路径追踪的时候也每次只做一次采样(只追踪一条反射光线),即用一次采样的结果来近似该积分。那么结果肯定是有很大偏差的,所以我们会发射多次光线来减轻偏差。 渲染结果还是会不可避免出现很多黑色噪点,随后便是用到降噪技术了。
总结来说,路径追踪比递归光线追踪更先进一步,更加真实去模拟光路传播行为,二者更属于递进的关系。而面对这种计算量,路径追踪还没法做到实时渲染。目前看来,路径追踪真是算得上光线追踪框架下的终极形态了。而路径追踪中用到的材质,其实很多现成的,例如Lambertian材质,其BRDF、PDF和采样函数这些东西我们都拿来即用,不需要去重复造轮子。
五、总结
看完这几类光线追踪,你或许会觉得,相比说对它们做区分,反而更像是不断往模拟真实光路传播这件事上的一步步发展。从光线投射开始,光线不会被反射,只是按照步长对光线采样;到经典的光线追踪,计算光线在其反射方向上的内容,以及探测阴影,但都算不上足够真实的模拟;到了递归式光线追踪,考虑光路的一步步反射,已经接近现实光路的传播方式;最后到路径追踪,考虑更真实的反射光路,考虑更真实的光量贡献。这是在光的反射这一点上不断深入探讨,从而形成了这么多类型的光线追踪,希望看完本文能够帮助你梳理清楚光线追踪这件事。