版权声明
- 本文为“优梦创客”原创文章,您可以自由转载,但必须加入完整的版权声明
- 文章内容不得删减、修改、演绎
- 本文视频版本:见文末
- 各位同学大家好,今天我要给大家分享的是光线追踪的原理和实现
- 大家知道在过往很多年里面,光线追踪技术一般只能用于离线渲染
- 但是由于硬件发展的速度非常快,大家知道摩尔定律是每一年半硬件的性能会增长一倍
- 但是现在像英伟达显卡,可能半年或者一年就会推出性能翻倍的显卡
- 所以已经完全打破了摩尔定律
- 那按照这个规律,可能也过不了多久,光线追踪技术就可以用在实时游戏当中
- 实际上现在有一些游戏像赛博朋克 2077 已经用上了支持光追技术
- 所以今天我就要给大家分享光线追踪的原理和实现,帮助大家能跟上未来图形学的发展
今天我们课程的议题主要有这样一些:
- 基本光线追踪的实现方式
- 包括基本光锥代码架构
- 光锥性能优化
- 光追的性能开销主要来源于跟踪的射线数量以及射线反弹的次数,那么如何对光锥进行性能优化呢?
- 光锥效果优化
- 光锥在实现效果的时候,算法特别简单,不需要去用各种各样的trick来做优化。但是由于光追的性能限制,如何在保证性能的前提下去优化效果实现,是一个问题
- 基于基本形体的光锥介绍
- 在前面,我会以一些简单的基本形体,比如:球形立方体去介绍基本光锥算法,但是如何基于任意物体表面来进行光线追踪呢?
- 光锥的镜面反射和后处理
- 我会讲到如何给光锥去添加镜面反射效果,而不仅仅是拥有自发光或者是漫反射
- 光追后处理
- 最后一步我们会讲到如果要做后处理,那么运用光锥应该怎么做。光锥做后处理跟前面我们讲的一样,就是它做起来特别的简单,不需要特殊的trick。
光追基本原理
- 光线追踪认为:我们屏幕上面每一个的像素,是我们在光线追踪时的目标方向
- 我们从摄像机所在的位置向屏幕的每一个像素点发射一条射线,并且沿着这条射线的方向一直行进
- 然后根据这个射线碰到了哪些物体以及碰到的这个物体反弹的光线信息,去决定最终我们在这个像素点的位置究竟应该如何着色
- 下面我们先从一个最物理的角度去理解一下光照的情形
- 大家看现在这个图,其实从原理上来说,从物理正确的角度来说,光是由于光源向360度角,没有特定方向性发射光线照射在物体表面
- 比如说这边这个光源,那么我从光源位置发射出一束光线
- 然后光线经过无数次的反弹,比如说它可能反弹到这个位置,然后就反弹到这个位置
- 经过无数次的反弹,最后假设有一束的光线最终射入到了我们的眼睛,那么我们就看见了光
- 大致的原理是这样
- 但是实际上如果我们这样去进行光线追踪的话,那么它的性能实在是太低了
- 因为你想一下,你的这个灯光要往360度角无死角的去发出光线
- 然后去追踪每一束光线它的一个反射再反射再反射的情况
- 即使不考虑反弹,由于它是向全方向去发射光线,它的计算开销仍然很大
- 所以实际上图形学当中,我们并不是这样去进行光对的
- 而是从我们的摄像机,也就是我们的眼睛去发射光线,然后计算光线经过的物体表面的颜色
- 比如说我们的眼睛看到了这样一块地板,这个地板是紫色的,那么好,我就知道在这个位置是可以获得一个紫色的光
- 那么类似的假设我的眼睛是照在这个位置,但是由于这个光线它的来源可能是另外一个发光的物体,所以我还需要把这个发光物体也考虑进去
- 我需要考虑灯光的多次反弹的组合的结果
- 这种时候地图图形学当中去计算光的方式,把这些光照信息记录在摄像机的投影面上面
- 这就是我们图形学当中进行光追的方式
- 好,现在我们具体分析一下光锥的技术实现
- 当我们从摄像机发射出一束光线,在进行光锥的时候,怎么样去获得光呢?
- 其实有 3 种可能性
- 第一种可能性是从光源位置发射出一束光,这束光没有照到任何物体的表面
- 这种情况就不要考虑,因为这个时候没有获得光照,也不需要去计算
- 第二种情况是射线刚好命中物体的表面,留在我们图上面这个红点
- 那这个时候我们的射线跟我们的物体表面会有一个焦点,我需要获取这个焦点的光照颜色信息,把它记录在这一条射线路径的光照的结果上面
- 第三种情况更普遍,就是从光源位置发出的射线经过物体,但是它不是刚好是照在物体的边缘上面,而是命中物体的内部
- 这个时候会有两个命中点,一个点叫入点,另外一个叫输点
- 这个时候光照计算的结果就有两个点在这里,我们不需要入点和出点 2 个解
- 为什么呢?因为在光追的时候,我追踪到入点的时候,由于我们的触点是不能够被我们的眼睛所看见的,所以触点对我来说是没有计算价值的
- 第一种可能性是从光源位置发射出一束光,这束光没有照到任何物体的表面
- 到这里,大家就已经完成了光追的第一步
- 在这里我们只绘制一个球体,不考虑光线的反弹
- 那么我们的代码结构大致上会是这样子的
- 首先我会去获得摄像机的试点,这个地方是获得我们观察的射线
- 假设我们的眼睛在这儿,我要观察球体的表面的像素
- 通过这句话我会把这个像素转换到 3D 空间
- 然后根据这个目标点以及我们的摄像机的原点,我们构造射线的方向
- 最后一行是我用来判断射线是不是跟球体发生了碰撞
- 我是把它封装到这个函数里面,叫做 CalculateRayCollision
- 如果要是发生了碰撞,那么我就取得发生碰撞的这个小球的颜色
- 如果是绿色,最后我们在这个像素点位置就渲染一个绿色出来
- 对于我们的蓝球和红球是同理的
- 首先我会去获得摄像机的试点,这个地方是获得我们观察的射线
光线反弹方向的确定
- 现在我们如果仅仅把一个小球以一种无光照或者说以一种固定颜色的方式去渲染出来,那很显然不是光追希望获得的结果
- 我们希望获得的结果是计算一束射线,就是白色这个射线照射在一个物体表面以后所获得的一个漫反射的信息,diffuse reflection
- 那么怎么样去获得这个漫反射信息呢?在这里是有一个难点的
- 对于我们知道漫反射是在物理上来说,它是朝各个方向不规则的光照反射结果的叠加
- 所以,如果你想真正的物理真实的去计算一个光锥漫反射,那么你需要把照射在物体的某一个点上面的所有反射光全部计算一遍,这样的话显然是不现实,它的计算代价太高了!
- 在这里,我们的光锥是运用了一个技巧,就是我随机的选择某一个方向,用这个反射的结果来代表漫反射的结果,那这样的话性能开销就会低得多!
- 虽然它的结果可能并不是那么令人信服,但是至少它是一个合理的解决方案
- 并且我在后面还会讲到如何对它进行优化
- 好,那么我们如何随机生成一个光照反射的方向呢?
- 在这里我们就需要在 shader 里面去编写一个产生随机数的算法
- 这个算法细节我们先忽略
- 那么来看一下,假设这个算法我已经写好了,那么我的算法大概会生成一个什么样的图像
- 那么我就会在屏幕上面的每一个像素点上面都去生成一个随机值
- 大家注意在这里因为反射的向量是一个单位向量,所以这个随机值介于 0 到 1 之间
- 如果每一个像素点是随机值用颜色输出,那么对应的颜色就说是在黑白灰之间
- 并且当我们随机生成反射方向的时候,我们可以传入不同的随机数种子
- 不同的随机数种子就会生成不同的反射图案
- 大家知道随机数种子是什么意思吗?
- 通俗来说,我们在计算机当中生成的随机数,其实并不是真正的随机的,它是通过一个表达式去计算出来的
- 你可以把随机数种子理解成数学函数 Y 等于 F(X) 里面的 X,你传入不同的 X 就得到不同的随机数 Y,这个 X 就是种子
- 因此比如说当种子值是1的时候,那么产生的序列可能是 ABCDEFG
- 而种子如果是3的时候,它会产生另外一个序列,可能是 CDGZRTX
- 这就是随机数种子的意义所在,就是输入不同的种子产生不同的结果
- 所以如果你想让光线追踪的结果是随机的,那么你可以输入不同的随机数种子
- 大家看左边和右边这两幅图像就是用不同的种子所生成的图像
- 但是大家知道光线反射的一个方向应该是一个三维向量
- 而我们这个图里面只是为每一个像素生成了一个值
- 所以实际上我们通常需要一次生成三个随机数,并且要对它进行单位化
- 就像我们下面这个图表示的一样,在这里每一个像素其实就代表了每一束光线照在物体表面以后的反射方向
- 左边这个图是在分辨率比较低的情况下生成的
- 这个像素的分辨率比较低,其实就是反射的方向比较少
- 右边这个图是在整个场景的分辨率调高以后生成的
- 你会注意到它输出结果就是一个密密麻麻的彩色的噪点图
- 每一个噪点就代表了每一个像素点的反射方向
- 这个就是我们在随机生成漫反射方向时候所需要的结果
- 这个图就是我们生成的小球反射方向的可视化表示
- 右边这个图我等一下解释,我们先看左边截图里这个算法:
- 刚才我们说因为需要介于 0 到 1 之间的值,并且需要三个,所以在这里我生成第一个值
- 这个地方是生成第二个值作为反射方向,X 方向这是 Y 方向,还有一个 Z 方向
- 但是因为这个随机值它是介于 0 到 1 之间的,而我们的反射方向应该是在-1到+1之间
- 所以在这里有一个乘以 2 减 1 的操作
- 并且虽然三个分量都是介于-1到+1之间,但是它们合起来以后形成的向量并不一定是一个单位向量
- 所以在这里我还要需要进行单位化
- 右边这个图输出的是反射向量的分布信息:
- 我前面讲过了,漫反射是随机选择一个反射方向
- 所以你生成反射向量的那个方向就会能获得更多的光线
- 因为你会去跟踪这个方向,结果就是这个位置比较亮,因为它照射的这个方向的光线反弹次数比较多
- 在这里 diffuse reflection 是随机的,这张图实际上就描述了反射向量生成的随机值的分布信息
- 大家会注意到这个分布是不均匀分布的
- 你可以在这里看到一个星星的标志,或者是一个奔驰的一个车标
- 这就说明我生成的反射向量并不是完全随机的
- 这也是光锥需要考虑的问题:
- 如果生成的反射向量不是随机的,刚才那么更多的获得反射向量的位置,就会产生更多的光照,就会更明亮
- 这个结果显然不是我们所希望的
- 所以在这里我们就需要对这个随机数的生成方式进行调整
- 那么怎么调整呢?
- 常见的随机数分布概率有两种:
- 一种是:均匀分布
- 像我们之前之所以生成了一个像外星人标志的反射方向的力度,是因为我们采用的随机数的生成方式是均匀零分布
- 当它作为球体的反射方向的时候,就得到了上面这种分布不均的结果
- 另一种是:正态分布
- 正态分布是说大部分的随机数都是分布在这个随机数范围的中心区域,而随机数范围的边缘区域获得的结果比较少
- 这个是我们要去做的一个调整
- 一种是:均匀分布
- 限于时间关系,我就不去一点点的去展示这个正态分布的算法了
- 相关内容的学习可以参考文末资料
好,我们来看一下这个最终的效果。如果你采用的是正态分布,那么你生成的结果是一个没有接缝的结果。那么学到这里,大家掌握光线追踪算法的基本框架已经有 50%了!
- 在这里咱们还有一个问题要处理:
- 就是假设我有一束光照在物体的表面的这个点上面,那么它反弹的方向不应该指向物体内部
- 除非这是一个半透明物体,它对光照具有一定的穿透性
- 对于一个普通材质的物体,如果不考虑透明性,光照就应该不会反弹到物体的内部
- 所以这个时候我们就要对这个光线反弹的方向进行校正,那怎么校正呢?
- 在这里告诉大家一个 trick,就是你直接取反取向量的赋值就可以了
- 如左图所示,白色表示入射光,绿色表示物体正面方向,黄色半球表示可反弹区域,如果随机生成的反弹向量为红色,则应调整为绿色方向
- 这里是我们在进行光线追踪的时候的一个算法的坑点
- 也希望大家通过我们这节课的学习能够有所了解,避免在具体实现这个光锥算法的时候遇到困难
- 恭喜大家又近了一步!
光追基本代码框架
- 最后我们来看一下光锥的基本代码框架,它代码框架大概是这样子的:(文字表现力有限,需要实时讲解请参考文末视频版本)
- 假设现在有一束入射光作为我们光锥函数的参数,传入到 Trace 函数里面去了(Trace 是跟踪的意思)
- 然后我们再传入一个随机数,这个随机数用来生成反弹向量
- 然后在这里我们会写一个 for 循环
- 为什么是一个循环呢?
- 因为刚才我讲过,一束光照射到一个物体的表面,它可能会一次反弹以后又碰到一个物体,然后还需要进行二次反弹
- 但是我们在真实的计算机世界当中,我不可能这样一个物体去无限的反弹下去,无限的去计算反弹,那这个性能开销是无限大的
- 所以说在这里我们可以设置一个最大计算的反弹次数
- 在这里如果是实时渲染,不是离线渲染的话,不会把这个值设的很高
- 为什么是一个循环呢?
- 在for循环里,我们通过调用下面这行 CalculateRayCollision 去传入射线,获得跟小球的碰撞信息
- 如果我们碰撞到某一个物体了,那么我就去计算它的下一次的碰撞
- 这个时候我就去构造我的这个反弹向量
- 在生成反弹向量的时候,我不会去直接找他碰撞了哪个东西,而是随机的去瞎碰
- 也就是函数 RandomHemisphereDirection(随机方向生成请参考《TA全栈》工程)
- 如果我们碰撞到某一个物体了,那么我就去计算它的下一次的碰撞
- 然后我们会进入到下一次循环
- 下一次循环的时候,如果没有碰到任何物体,也就是if条件不成立以后,那这次光锥就结束了
- 如果反射向量继续与场景物体碰撞,那就会再次进入if语句,计算下一次反弹
- 当然前提是它在你的设定的反弹次数以内
- 所以大家会发现光追的一个坑点:
- 如果反弹向量刚好命中物体,那在这个点上面你计算出来的反射就会更强烈
- 如果没有,则反射就会更弱
- 所以,光锥为什么产生的结果是随机的,是有瑕疵的!
- 下面我们来看下渲染效果(详细版看文末视频参考)
这是场景的基本设置(非光追渲染效果)
这是根据基本设置,将中间的小球设置为发光体的光追渲染效果
这是不同光线反弹次数情况下,光追效果的变化
可以看到,反弹次数越多,小球之间互相收到光照影响就越明显,但性能开销也越大
光追的8个进阶要点
上面主要分享的是“小结”中的第一点“基本光线追踪》
文字表现力较弱,其余内容可以看我的视频版本分享
这里也给大家概括了一些光追进阶点,供大家进一步参考:
参考
- 完整视频请点击本链接观看:【TA技术美术进阶】光线追踪:原理和实现_哔哩哔哩_bilibili
- 更多技术干货请加本人主页头像