通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理

本文适合完全不了解渲染又想略窥一二的兴趣朋友,我也非大牛,仅写下自己粗略的理解,内容上难免会有错漏,如果你发现有误的地方,欢迎交流修改。文中部分图片来源网络,如侵权请联系,我马上修改。

1、让你做渲染,你首先会怎么做?

你如果想用编程的方式渲染一副图像,例如800x600分辨率的图像,也就是该图像一共有800x600个像素点,现在要你用原始的方法,使用C++去画这副图像,你会怎么做?

先假设要你画一副全是黑色的图片,用最笨的方法就是,用两个for循环,第一个for循环遍历800,里边再套第二个for循环遍历600,相当于遍历一个800x600大小的二维数组,然后给每个数组元素填上或者输出一个RGB值,也就是像素的颜色值,黑色的RGB值为(0,0,0)。以上过程相当于你手工逐个给像素点填上黑色,结果可以称为你渲染了一副800x600分辨率的黑色图像:
通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理_第1张图片

当然你也可以在遍历到某些特定的像素时填别的RGB值:
通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理_第2张图片
就像你在刷算法题时遇到的画棱形的问题:
通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理_第3张图片

2.目前主要有什么方法可以做渲染?

现在渲染当然不是用上面说的方法,现在渲染用到的方法,主要分为光栅渲染光线追踪渲染

这里的区分是从编程实现原理的角度上看的,不是指现在拿个Blender那种工具做渲染的,不是指在交互窗口上摆好模型的位置,设置好光源等然后渲染画出一副精美的图像。这种软件上做的渲染,都是用着人家写好的渲染器上面做的,交给用户使用就行了,是指一种功能。

而这里介绍的渲染是指从无到有地画出一副图的过程,光栅和光线追踪就是实现这个过程的方法。

3.光栅化是什么?

光栅化,英文是Rasterization,也叫光栅渲染。目前我们用的一些可视化工具,包括大多数游戏,都是用光栅渲染。

那怎么理解光栅呢?光栅可以理解由显示屏上一个个发光点组成,而在电脑上创建的三维物体或场景,需要显示在屏幕上,而想办法将三维物体显示在屏幕一个个发光点上的过程,就叫对三维物体进行光栅化。借用网络的图片:
通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理_第4张图片

光栅化是怎么实现的呢?核心一个词就是投影

这里要先说明一些,我们创建的三维物体是连续的,怎么理解连续这个词呢?我们自己在一张白纸上,画两个点,将这两个点用一笔画直接连接起来,这就是“连续”的意思,这条线中间是没有断点的:(屏幕显示的内容总是离散的,所以这张图片里展示的“连续”是概念上的说明)

通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理_第5张图片

如果我们改成在两点之间用笔点出很多个点,虽然都是单独分散的点,但是放远看了,发现这一堆点也近似是连续的一条线,但是本质上这条线是一个个分散点组成的,这条线就是指“离散”的。(实际上图片里的离散点应该要更密集)
通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理_第6张图片

虽然与一笔画出来的线都是长的几乎一个样,但是他们却分别是“离散”和“连续”的线,属于是不等于的关系。也可以理解为,“连续”是一条函数曲线,区间内每个x点都可取到y值,而“离散”是对这条曲线的固定步长的采样结果。

“离散”的屏幕无法直接显示“连续”的三维物体,因此需要先对连续的物体进行“离散化”,离散化就是对三维物体的采样点进行投影,结果存到帧缓存上(存储一帧画面的像素值),再将帧缓存交给显卡,显示一副图像。借用网络的图片:
通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理_第7张图片

投影是指将三维物体投影到二维平面上,要显示一个800x600大小的窗口,就创建一个800x600大小的数组(该数组可称为帧缓存),投影就是将三维物体上的某个采样点的颜色映射到二维数组的元素上。

这里简单说一下,三维物体上的颜色是怎么来的呢?

就简单而论,你就试想这个三维物体是个球体,球体就长红色的,每个地方的RGB值都是(255,0,0),复杂而论,就是三维物体上的每个顶点的颜色值都是用着色器计算出来的,属于更进一步的内容,这里了解即可。着色需要在投影前完成。

好了,那么投影是怎么做呢?这个就是借助投影矩阵了(涉及线性代数):推荐:投影矩阵的推导

投影矩阵是根据我们场景中的虚拟摄像机的位置来确定的,虚拟摄像机好比我们人的眼睛,它的位置决定了我们的观察视角,例如我们玩CF、使命召唤这种第一人称游戏,我们看到的游戏画面就来自虚拟摄像机,我们用鼠标可以控制摄像机转向,用方向键可以控制摄像机的移动,而投影矩阵就是根据这个摄像机的实时方位生成的,那怎么生成的呢?其实就是一套固定的矩阵的模板(百度搜到直接套上去也行,自己理解了原理自己写也许,相当于一个数学公式),在矩阵上填上参数就行了。(一般像OpenGL这类API,会直接提供现成的创建函数,开发者填参数就行)

透视投影的投影矩阵长这样:
通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理_第8张图片

有了投影矩阵,就能将三维物体上的颜色值映射(数学上叫线性变换,其实就是矩阵运算,套公式的事)到我们的帧缓存上,然后将这个帧缓存交给我们的GPU来读取显示到显示屏上,我们就看到啦,这就叫光栅渲染啦。

借用网上的好图:透视投影的示意图以及投影到平面成像的示意图。
通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理_第9张图片

至于为什么现在游戏画面越来越逼真了,比老游戏的画面效果好那么多,这个主要是由渲染方程决定的,渲染方程就是三维物体上每个顶点颜色的计算公式。

4.光线追踪是什么?

要是能理解上面的光栅渲染,那光线追踪也不难理解。

光线追踪不是一项新潮技术,它最早在80年代就被提出来了,而且这么多年也不乏研究,这种渲染方法更多用在动画渲染等一些不需要实时展示画面的应用上。

那为什么光线追踪在近三年你才看到有相关的新闻呢?这是得益于英伟达20系显卡RTX技术的推出后,将普及光线追踪实时应用变为可能,光线追踪才出现在大众的眼中。20系显卡用到的图灵架构能够便于做光线追踪,图中RTX OFF可以看成光栅渲染的游戏画面,而RTX ON可以看成用了光线追踪的游戏画面。

通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理_第10张图片

但是以前因为电脑硬件性能的不足,面对计算量庞大的光线追踪,只能做到花一段时间来渲染一帧图像,而玩游戏现在都要求一秒60帧了,甚至电竞都要一秒120帧起步了,但是游戏用光线追踪渲染的话,连一秒一帧都做不到,计算眨眼补帧也是无法让人接受啊,因此以前一般用光线追踪来渲染出版物上一副精美的图像,或者一部画质强大的CG动画。

说回正题,怎么理解光线追踪呢

试想一下,我们人眼能看到外面世界,是因为有光,如果我们晚上在封闭的房间关掉所有的灯,那我们啥也看不见,是因为没有光进入我们的眼睛。

光线追踪就是想模拟这个情景,追踪每条光线的传播行为,计算每条光线对我们人眼观察的贡献值,即颜色值。

通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理_第11张图片

那要怎么模拟?光线那么多条,追踪得完吗

的确,就像上图那样,场景中有灯光,太阳光等许多光源,而且每个光源可以发射无数条光线,要是每条都追踪的话,先不说计算量的问题,就连怎么用代码去实现这个模拟的过程都让人觉得头痛。

实际上,现实世界中存在那么多光线,真正帮助我们眼睛里成像的只有射入我们眼睛的那部分光线(上图的红色光线),而没有进入我们眼睛的光线(上图的黑色光线),则是没有用的。

因此,我们要模拟的只需要模拟那些进入我们眼睛的光线

考虑到光路是可逆的,也就是说从光源到我们人眼的这条光线的光路,不管是正着走还是逆着走,这条光线都是一样的。可以理解为,你利用这条光线进行计算,就算把它的起点和终点对换,也不影响你的计算结果。

通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理_第12张图片

所有我们模拟光线传播过程的一个很巧妙的设计就是利用了光路的可逆性,将光线的起点改为从我们人眼出发,去寻找场景中的光源,这样连起来就是一条从连接了人眼和光源的光路,是一条有用的光线。

通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理_第13张图片

这样就将问题从原来多个光源对应多个发射点,简化成了仅有我们人眼一个发射点,不管场景中布置了多少个光源,我们都只要考虑从我们人眼出发追踪光线就行了。

所以光线追踪也叫“反向光线追踪”,展开来说就是“追踪从我们人眼发出然后射到光源的光线,模拟这个过程来进行真实感渲染”。出现了反向这个字眼,相对的就会有正向,正向光线追踪那自然就是指追踪从光源发出的光线,但实际上,并不需要用到这个所谓的正向光线追踪。你所看到的光线追踪的概念,统一是指反向光线追踪。

光线的传播行为大致分为直射、反射和折射,直射自然是我们眼睛和光源直接没有遮挡,光线能直接传播,而反射和折射就是光线碰撞到物体后,接着向新的一个方向走。用数学来表现光线的反射和折射其实你肯定也很熟悉:

通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理_第14张图片

而其中我们最重要模拟的就是光线的反射,模拟光线是怎么反射的,就这一项就有非常多的研究。

其中我们最容易理解的就是镜面反射,这就举例如何根据镜面反射来计算光线的反射方向:

我们假设显示世界就是一个三维坐标系(有x,y,z三条坐标轴),然后定义一条光线就是一个三维向量(有x,y,z三个分量),也称三维矢量。当光线碰撞到平面上的一个点,那就计算这个点的法向量(即垂直平面的向量,同样是三维向量),然后利用该法向量来计算这条光线的相反方向(依然是三维向量),这个跟我们中学物理学习的理论是一致的(原理如上图),然后我们沿着这个新的方向继续为这条光线寻找光源,整个计算过程实则就是三维向量的计算。这已经就是模拟追踪光线的过程,虽然简单,但就是基础。
通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理_第15张图片

那光线追踪的理论大致如上,其实原理很简单,关键是怎么用光线追踪来做渲染呢?

我们就将以上模拟过程中出现的东西,准确表达出来。首先是我们的眼睛,就定义为一个虚拟摄像机,在三维坐标系中有位置坐标,镜头朝向的向量,这些都是可以我们自己给具体参数,然后我们看到的画面就是假设是一个800x600的数组,我们要做的就是为每个数组元素填上像素颜色值,也为其定义相对我们摄像机的坐标位置,相当于我们摄像机移动的话,画幅也会跟着移动(这里涉及世界坐标系和摄像机(局部)坐标系的知识)。模拟光线的话,我们就从我们的眼睛出发,穿过800x600中的每个像素点,发射光线进入场景中,起点就是我们摄像机的位置,目标就是每个像素点对应的坐标,然后用目标坐标减起点坐标就是这条光线的方向。追踪光线可以以我们上面说明的镜面反射为例,追踪我们发射出的光线能不能最终到达光源,光源在场景中可以是一个也有它的坐标位置,光线能够到达光源是指光线与光源发生了碰撞。
通俗傻瓜式理清光栅化渲染和光线追踪渲染的原理_第16张图片

注意:这里的光线原理图可能会跟你在别处见过的有区别,但是都是属于光线追踪的原理,因为光线追踪可以分类为很多种,但是归根结底都是要靠追踪光线来实现,上图也是我根据本文说明来画的,具体的光线追踪分类我另外写了一篇详细介绍的文章:各类光线追踪原理介绍,我这张原理图你可以记为“递归式光线追踪”,也是我们平时说到光线追踪时实质所指的原理。

然后分情况处理,如果能就计算到达光源后的颜色,如果不能到达光源就计算无法到达光源的颜色,要问具体怎么计算的话,交给渲染方程,就是套一下数学公式而已(大概意思是会根据当前光线的方向向量来计算这条光线对应的颜色)。

这里插一句,根据上图来看,光线要么射空,要么反射,但是光线不能碰到物体都永无止境地反射下去,因为这样是算不完的,我们可以自己设定一个阈值,就是光线反射次数超过3次还没命中光源的话,我们就判断它没法找到光源,停止继续反射。

我就举一个最简单的例子,我们假设光源就在(0,0,1)这个坐标位置,光源是白色,RGB值是(255,255,255),然后有一个小球在(1,0,0)的位置。我们追踪光线,如果最终能够与光源碰撞的话,那我们这条光线对应的像素点就填上白色(我们的光线当初发射时是穿过每个像素点的,像素点对应800x600二维数组里的数组元素),如果光线反射超过3次了或者射空的话(射空指没有与任何东西碰撞),那就给这条光线对应的像素填上黑色,RGB值是(0,0,0),等遍历为800x600个像素点后,就是处理完800x600条光线后,我们就能得到一副图片,这副图片的背景是黑色的,但是我能看到一个白色的光源,也能看到一个小球,小球背向光源的部分是黑色的(因为这里的光线反射后最终也没有与光源碰撞),而这副图片就可以说是你用光线追踪渲染出来的。

当然最初的光线追踪还有目前的光线追踪并非完全是我上面提到那样去模拟去实现的,但是原理是这么个原理,基础也是这么个基础,我也打算再写一篇文章专门通俗地理清光线追踪中的分类。但是目前对光线追踪的入门理解,可以根据我上面介绍的去理解。

5.光栅渲染和光线追踪渲染有什么不同?

其实目前游戏里即使用这光栅渲染,得到的画面效果也不比光线追踪差,因为这是渲染时用到的成像方法的不同,决定画面是否逼真的,最关键还是要看渲染方程,要看每个像素点的颜色值是怎么算出来的。

但是光线追踪自然有它的优势,因为它就是模仿真实光线的传播行为,因此给很多光影效果的模拟提供巨大的方便,在物体阴影上,在镜面反射的内容表现上,光线追踪都能以很自然地方式去模拟,代码实现上也是非常自然便利的(意思是在计算的时候,很多要用到的数据随用随有)。

而要用光栅渲染去实现同样的阴影效果,碍于成像方法的不同,该计算过程会变得非常复杂,难以实现(缺少要计算的直接数据,要绕路子或者费劲去算出需要的数据)。

所以这两种渲染方法最显著的差异就在于成像方法不同所带来的计算便利性,这种便利性能够极大地影响代码上和计算上能够模拟真实光照的程度。

既然我说光线追踪能够提供这种便利性,那为什么之前还说光线追踪因为计算量庞大导致一直无法在实时应用上发挥作用呢?

其实光线追踪中最耗时的部分在于,追踪光线的时候需要计算光线与场景中的哪个东西碰撞了,这一点我在介绍光线追踪的时候没有铺开说。

试想一下,一个游戏场景,每个物体都是有三角形面片组成的,三角形面片少了,那么一个球体看起来也会变得有棱有角的,现在游戏里的场景这么精密,三角面片的数量庞大得令人难以想象。而计算机又是笨拙的,计算光线与谁碰撞需要一个个问我有没有撞上你呢?(即求交计算),而得益于“加速结构”的帮助,可以让光线不需要与全部三角面片做求交计算,只需要与局部面片求交即可。

但是现有游戏画面分辨率少说也1980x1080了吧,而且实际上穿过每个像素点一般可不只是发射一条光线,每条光线在场景中也不只是仅反射3次那么少,这些计算量一叠加上来,将使得一帧画面的计算也变得非常缓慢,因此说光线追踪的计算量复杂。即使现在发展到英伟达30系的显卡了,在游戏里也做不到全画面全场景去做光线追踪渲染(现在游戏支持的光线追踪渲染都不算是完全体的光线追踪,通常是结合了光栅渲染的)。

6.现在用代码是怎么做光栅渲染和光线追踪渲染的?

渲染一般用到的语言是C++,通常可以在Visual Studio 2019上写。我们现在写渲染代码,不需要自己完全去实现一遍上面介绍的底层原理,通常都是直接使用现有的框架(或者说API,或者是工具包函数,工具就是指一个个写好的函数/方法),光栅渲染一般使用OpenGL,或者DirectX,我看很多地方介绍都会指出很多人误以为计算机图形学就是OpenGL,是的,OpenGL其实就是个API,一个工具,用来帮助你写渲染代码的,不需要从0实现一遍光栅渲染,你要用投影矩阵是吧,直接调用OpenGL里面的函数,给参数就行,你要创建一个球体是吧,直接调用里面的函数,给参数就行,大概就这么个意思。

而光线追踪因为国内接触这块开发的也少,使用到的框架可能也未必耳熟能详,有英特尔的embree和ospray,有英伟达的optix,也有支持光追的vulkan,而且DirectX 12也支持光线追踪了,但是代码貌似不好写。embree是个相对更底层的框架,光线是怎么追踪的,光线怎么反射,渲染方程是怎么样的,都得自己去实现,用来写一个光线追踪渲染器最适合不过了。我的博客里有embree相关的介绍和使用教程,感兴趣的可以翻去了解一些。我比较推崇使用OSPRay或者Optix。

你可能感兴趣的:(光线追踪,图形学,3d渲染,可视化,游戏引擎)