昨天翻 Unit Europe 2017 发现一篇很有意思的演讲,作者因为游戏的特殊性,彻底摒弃了 Unity 原本的烘培系统,通过只烘培 Light Probe 的形式自己读取数据建立全局光照,文章提到了 GDC 的一篇讲 Light Probe 插值的演讲,过去翻了翻,果然,LightProbe里存储的是烘培后用球谐函数编码的全局光照,正好趁机会把球谐光照这块梳理下:
理论出自02年SIG的一篇论文,后面有人做了详细的笔记,国内也有博主翻译了一部分,不过并不详尽,因为大部分底层计算工作 Unity 已经帮我们做好了并提供了接口,因此并不需要我们自己实现,这里主要讲一下我自己认为比较核心的几个点的理解:
1、蒙特卡洛积分:
将计算机尤其是GPU上非常难以计算的积分简化为了加法,这是球谐光照的前提
2、投影:
球谐光照的实质就是将复杂的光照信号投影到基函数上存储,然后在使用的时候再将基函数上的数据加起来重建光照信号
3、伴随勒让德多项式
想比如正弦信号,伴随勒让德多项式作为基函数不仅是正交的,而且是归一化的, 这意味着其具有旋转不变性,适用于动态物体
Unity 使用了三阶的伴随勒让德多项式作为基函数,因为Unity 主要用来存储全局光等低频信号,其在欧拉坐标系下如下所示:
以法线方向作为
Unity通过烘培时的光线追踪计算出其光照原始信号,然后投影到基函数并存储其系数,我们在Shader中可通过 ShadeSH9 函数获取重建信号,ShadeSH9 实现在 UnityCG.cginc 文件中,具体代码如下:
// normal should be normalized, w=1.0
half3 SHEvalLinearL0L1 (half4 normal) {
half3 x;
// Linear (L1) + constant (L0) polynomial terms
x.r = dot(unity_SHAr,normal);
x.g = dot(unity_SHAg,normal);
x.b = dot(unity_SHAb,normal);
return x;
}
// normal should be normalized, w=1.0
half3 SHEvalLinearL2 (half4 normal) {
half3 x1, x2;
// 4 of the quadratic (L2) polynomials
half4 vB = normal.xyzz * normal.yzzx;
x1.r = dot(unity_SHBr,vB);
x1.g = dot(unity_SHBg,vB);
x1.b = dot(unity_SHBb,vB);
// Final (5th) quadratic (L2) polynomial
half vC = normal.x * normal.x - normal.y * normal.y;
x2 = unity_SHC.rgb * vC;
return x1 + x2;
}
// normal should be normalized, w=1.0
// output in active color space
half3 ShadeSH9 (half4 normal) {
// Linear + constant polynomial terms
half3 res = SHEvalLinearL0L1(normal);
// Quadratic polynomials
res += SHEvalLinearL2(normal);
if (IsGammaSpace())
res = LinearToGammaSpace(res);
return res;
}
三阶的基函数系数分别用了两个子函数来读取,其中
// SH lighting environment
half4 unity_SHAr;
half4 unity_SHAg;
half4 unity_SHAb;
half4 unity_SHBr;
half4 unity_SHBg;
half4 unity_SHBb;
half4 unity_SHC;
是 UnityShaderVariables.cginc 中的内置变量,用来存放存储的系数,在实际使用中,我们直接调用 ShadeSH9,配合LightProbe 即可读取到 precompute bake 生成的自发光信息