前言
全文采用通俗语言,不罗列数学知识和专业知识,仅分享自己的心得。
本文为什么说是“各类光线追踪”的原理呢?是因为我当时初识光线追踪的时候,从很多博客或者国内外论文上,都会看到光线追踪原理的介绍,但是经常会存在或多或少的差别,对于刚接触光线追踪渲染的人来说很容易被搞懵。经过研究生阶段我一直在接触学习光线追踪,所以对光线追踪不敢说有很深入的认识,但是也有一点小小的心得,因此想将光线追踪的原理分门别类地说清楚。
一、关于光线追踪原理的说明
光线追踪的实现原理,不是像我们平时课程学习时遇到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帧每秒,而且虽然图像质量更好了,但是放大细看还是不经看,其实噪点依然不少。
再看回本节开始的那张玻璃球的图像,是不是觉得这么高质量可能需要最少五六十次的采样?其实不是的,因为增加采样数只是一种最基本的降噪方法而已,还有很多降噪方法可以更高效地完成这项工作,例如前沿的会直接用上深度学习降噪。
再说另一个细节,或者问大家一个问题:有没有发现上面案例里都用了镜面反射?
那现实场景中,怎么可能都是镜面反射,还有漫反射呢?那要怎么模拟。
那就涉及到材质了,我们生活中有毛皮材质,有磨砂材质,有镜面材质。我们要在代码里面去模拟材质,就是要模拟光线遇到不同材质时的反射方向,也就是我们定义不同种类的材质,为每种材质定义计算反射光方向的方法公式(关键点),再将材质赋予给场景中不同的物体。那么我们再去做光线追踪时,光线就能在每个碰撞点处计算出相应材质下的反射方向。既然我们模拟了真实的光线传播行为,那最终渲染的效果(即我们眼睛看到的图像),就能同样看到真实的磨砂、毛皮的效果。
计算机图形学发展了那么多年,什么材质下怎么去计算反射方向,其实早就已经被提出和不断改善,其实就是一个计算公式,到今天各位直接拿来用即可,这种公式被命名成一个专业术语——双向反射分布函数,Bidirectional Reflectance Distribution Function(BRDF)。BRDF有一些经典模型(或者说经典的公式),例如Cook-Torrance模型,也有很多研究工作提出自己的BRDF模型发表论文,都是现成公式,在实际开发中,可以直接套公式使用。
如果读到BRDF感兴趣的话可以去深入了解一下辐射度量学这方面的知识。BRDF属于光学的知识,只是在光线追踪中可以发挥具体的重要作用。但是作为了解递归光线追踪入门的话,就只需要了解在光线追踪在,BRDF就是用来计算光线是怎么样反射的,输入入射光方向,得到出射光方向。
版权声明:本文为CSDN博主「864306337」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_37366618/article/details/123504877