【转载】“最简单的” Unity 光照探头总结

原文链接

版权声明:本文为CSDN博主「zengjunjie59」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/zengjunjie59/article/details/123899729

原文写得很好,我对文章的格式进行一些调整,并修正+补充了一些内容

正文

作用

PBR 流程中,光照探头用于在 光照没有变化 的情况下,给 动态 物体提供 间接光漫反射 信息。

内容对应某位大佬的视频:Light Probes 基本理论介绍_哔哩哔哩_bilibili

整个流程

第一步:记录所有探头的数据

在烘培的时候,光照探头需要记录周围 360 度的颜色信息。类似 反射探头 的 cubemap ,但是由于光照探头非常多,而且 cubemap 占用内存太多,所以实际储存 不可能 用 cubemap ,而是储存着 球谐函数某几个基函数的系数。当实际渲染进行采样的时候,用这几个系数和球谐函数基函数来 还原 周围的颜色信息。这里的思想和 傅里叶变换 是一样的,也是用几个简单的信息基函数,通过不同的系数来还原一个复杂的信号。

  • 傅里叶变换 如下图:
    image.png

c1 是第一个基函数的系数,f1 则是第一个基函数。通过这种组合方式还原出复杂的原信号。

球谐函数 也是,通过

系数 1 ∗ 基函数 1 + 系数 2 ∗ 基函数 2.... 系数1*基函数1+系数2*基函数2.... 系数1基函数1+系数2基函数2....

的形式来 还原 出光照探头原本位置的 Cubemap 颜色信息,只不过球谐函数是建立在 球坐标系 上的,而傅里叶变换则是建立在 二维笛卡尔坐标系 上的,当然,这并不能百分百还原。总的来说,烘焙 光照探头就是 记录几个基函数的系数 以此来记录探头周围的颜色信息。

  • 下面是各种球谐函数的基函数示意图:
    image.png

图中总共显示了四层球谐的基函数,第 0 层只有一个基函数,还原度最低,第 1 层,则有左右、上下、前后三个基函数,而下面的层数则更加的细致,还原度会更高,但是随之的性能消耗也越大

Unity 里通常会使用前 3~4 层的基函数。

下面是还原示意图:

image.png

image.png

左下角的图,是球谐函数还原后的颜色信息,颜色越亮,高度越高。而右下角则是原本的 cubemap ,由于这里是用来做 间接光漫反射 的,所以肉眼是区分不出来的。能够有效的减少内存的消耗。

第二步:对光照探头进行采样

在渲染中,计算某个片元的 间接光 漫反射 颜色的时候,会取该片元 附近(半径,该值可由程序员自定义)的所有 光照探头,传入一个向量对每个光照探头进行采样,然后进行加权平均,越靠近片元的探头,权重越大

如何对光照探头进行采样

image.png

如上图片元 A A A,会取到半径内的探头 p 0 p_{0} p0 p 1 p_{1} p1。传入一个向量对 p 0 p_{0} p0 p 1 p_{1} p1 进行采样,再计算平均值即可。但是会遇到 两个问题,如下:

视差问题

当传入间接光入射光方向的负方向( ω \omega ω)的时候,由于片元 x x x p 0 p_{0} p0 p 1 p_{1} p1 的位置不一致,所以并不能在光照探头中直接用传入的向量,而是需要进行 raycast(射线检测)来算出 ω \omega ω 与场景物体的交点,再利用交点与 p 0 p_{0} p0 p 1 p_{1} p1 的球心算出对应探头的采样向量。利用这个向量对光照探头进行采样,各个光照探头采样出来的颜色加权平均后就能得到交点的颜色了,那么这个颜色也就是 x x x 片元反射出来的 间接光 漫反射 颜色

image.png

遮挡问题

如下图 p 1 p_{1} p1 被挡住了,对 p 1 p_{1} p1 探头进行采样,是 不能正确 得到图中的 Γ \Gamma Γ 点的颜色,所以这个时候需要剔除掉 p 1 p_{1} p1

image.png

为何可以利用加权平均得到光照颜色的原理

image.png

  • 这里的 v v v ,在其他文章里可能被写为 w o w_{o} wo
  • 右边半球图的 w i w_{i} wi 应该是入射光线,图上却画成出射光线

上图是 PBR 流程的基础函数,除去 自发光 部分,也可用到计算 间接光 漫反射 上。

当处理 漫反射 的时候,图中的 f ( w i , v ) f(w_{i},v) f(wi,v)(即 BRDF 函数)可以提取到积分外面。

下图是 BRDF 函数的说明:(具体学习可以去:【学习笔记】Unity PBR的实现 - 知乎

f ( l , v ) = d L o ( v ) d E ( l ) f(l,v)=\frac{dL_{o}(v)}{dE(l)} f(l,v)=dE(l)dLo(v)

  • 辐照度:单位时间 单位面积 受到的辐射能量
  • 辐射强度:单位时间 单位立体角 受到的辐射能量
  • 辐射率:单位时间 单位立体角单位面积 受到的辐射能量

详细点写 BRDF 其实就是:

f ( l , v ) = k d c π + k s D F G 4 ( w o ⋅ n ) ( w i ⋅ n ) f(l,v) = k_{d}\frac{c}{\pi}+k_{s}\frac{DFG}{4(w_{o} \cdot n)(w_{i} \cdot n)} f(l,v)=kdπc+ks4(won)(win)DFG

翻译一下 就是:

漫反射比例 表面颜色 π + 镜面反射 法线分布函数 ∗ 几何函数 ∗ 菲涅尔系数 4 ( v i e w D i r ⋅ n o r m a l ) ( l i g h t D i r ⋅ n o r m a l ) 漫反射比例\frac{表面颜色}{\pi}+镜面反射\frac{法线分布函数*几何函数*菲涅尔系数}{4(viewDir \cdot normal)(lightDir \cdot normal)} 漫反射比例π表面颜色+镜面反射4(viewDirnormal)(lightDirnormal)法线分布函数几何函数菲涅尔系数

因为是 漫反射,所以 镜面反射 可以 忽略,然后把 漫反射 部分提取到积分外

把 BRDF 提到积分外的时候,函数变成:

L o ( x ) = ρ π ∫ Ω L i ( x , w i ) ( w i ⋅ n ) d w i L_{o}(x)=\frac{\rho}{\pi}\int_{\Omega}^{}L_{i}(x,w_{i})(w_{i}\cdot n)dw_{i} Lo(x)=πρΩLi(x,wi)(win)dwi

其中的 ρ π \frac{\rho}{\pi} πρ 就是提取后的 BRDF,里面的 π {\pi} π 和能量守恒相关。

第三步:解决积分困难问题

由于计算积分非常困难,所以把积分的形式变成 由几个精确光照效果的累加,一个光照探头就是一个精确光照,改变后的函数如下图:

image.png

其中计算某个光照探头的 权重 的函数则是:

w ( t ) = 2 t 3 − 3 t 2 + 1 w(t)=2t^{3}-3t^{2}+1 w(t)=2t33t2+1

w ( t ) w(t) w(t) 是权重, t t t 就是探头与片元的距离除以半径, t t t 范围是 0~1 之间。

  • 权重函数图像:
    image.png

内置管线的福利:ShadeSH9

在 Unity 内置管线中,只需要调用 ShadeSH9(float4(i.normal, 1)),传入片元 世界空间 下的 法线方向 即可得到该片元 间接光漫反射 颜色。

源码分析

可以看一下这篇文章:Unity3D的全局光照和阴影:下篇(unity3D中的球谐光照和SH球谐函数、unity实时阴影抗锯齿解决方案) | 电子创新网 Imgtec 社区

补充:光照探头的基本思想

对于 GI 的计算是一个典型的球面函数。我们很难实时的根据这个公式去计算一个点的光照,因为这个 太复杂 了,但是通过球面调谐函数,我们可以通过找到 几组简单的基底函数,来 尽量 拟合 这个过程。
例如在 光照条件固定光源如果变化需要重新采样) 的情况下,我们可以对每个空间点附近的一个球面区域去真实的用公式计算一些采样点的值,然后按照光照公式的采样曲线同选定的几组球谐函数正交基底积分算出每个正交基底的参数(烘焙),最后利用这些正交基底,即可以在运行时近似求出空间点球面上 任意一个位置 的光照。

这就是 光照探头的基本思想,即在 光照固定 的情况下先用真实光照方程采样(烘焙),降低维度,在运行时用一组少量参数去计算任意位置的光照(比如,调用 ShadeSH9(float4(i.normal, 1)))。

总结

球谐函数 酱!妈咪妈咪哄~ 趁光源还没有变化,快快变一个删减版的 Cubemap 出来,我要立刻在此处 “采花” …… 啊呸!采样光照信息!