GameTech/URP 渲染实战

案例 8.1用 C# 构造环境光:Color、Gradient 与 Skybox入门

目标:不经过 Lighting 窗口,直接用脚本构造三种来源的球谐环境光,对比它们对物体的影响。这也是在运行时自定义环境光(比如昼夜循环)的基本手段。

SphericalHarmonicsL2 是 Unity 提供的三阶球谐结构体,有两个方便的方法:AddAmbientLight(color) 加一个常数项,AddDirectionalLight(dir, color, intensity) 加一盏方向光。“Color”模式只需要前者;对于 Gradient 和任意的天空函数,我们在球面上均匀撒点,把每个方向当成一盏小方向光加进去——这就是蒙特卡洛投影:

Assets/Cookbook/Ch08/EnvironmentTools.cs(节选:project)
/// 蒙特卡洛投影:在球面上均匀取 sampleCount 个方向,每个方向当成一盏小方向光加进 SH。
/// AddDirectionalLight 内部已经做了余弦卷积,但它的归一化是“在灯光方向上求值 = 2 × 强度”
/// (实测:AddDirectionalLight(up, 白, 1) 在 up 方向求值得 2.0)。而三阶截断的余弦波瓣在峰值处
/// 理论值是 1/4 + 1/2 + 5/16 = 17/16,所以 Unity 的结果比“辐照度/π”大 2÷(17/16) = 32/17 倍。
/// 均匀环境 L 拆成 N 盏灯时,每盏取 4L/N 才能得到 L,再乘 17/32 抵消上面的倍数。
public static SphericalHarmonicsL2 ProjectToSH(Func<Vector3, Color> radiance, int sampleCount = 4096, int seed = 1)
{
    var sh = new SphericalHarmonicsL2();
    var rnd = new System.Random(seed);
    float weight = 4f / sampleCount * (17f / 32f);
    for (int i = 0; i < sampleCount; i++)
    {
        float z = (float)rnd.NextDouble() * 2f - 1f;
        float a = (float)rnd.NextDouble() * Mathf.PI * 2f;
        float r = Mathf.Sqrt(1f - z * z);
        var d = new Vector3(r * Mathf.Cos(a), z, r * Mathf.Sin(a));
        sh.AddDirectionalLight(d, radiance(d), weight);
    }
    return sh;
}

权重的推导值得一看。假设环境是均匀的、辐射率为 L,我们希望 SH 在任何方向上求值都得到 L(URP 约定下的“辐照度 / π”,下一节解释)。把它拆成 N 盏均匀分布的小方向光,每盏强度 w,对法线 n 的 Lambert 响应总和是 w∑imax(0,n·li)≈wN·14(max(0, cosθ) 在整个球面上的平均值是 1/4),令它等于 L 得 w = 4L/N。但实测 Unity 的 AddDirectionalLight(up, 白, 1) 在 up 方向上求值得到 2.0,而三阶截断的余弦波瓣在峰值处的理论值是 1/4 + 1/2 + 5/16 = 17/16——Unity 的结果整体大了 32/17 倍。所以代码里再乘 17/32 抵消。这类“约定上的倍数”在不同引擎、不同 API 之间经常不一样,写完投影代码后务必用一个均匀环境验证一次:本例中用白色均匀环境投影后,Evaluate 在各方向上都应得到 1 附近的值。

Gradient 的环境函数只是按方向的 y 分量在三种颜色之间插值(Unity 内部的具体插值曲线未公开,这里用一个形状相近的函数):

public static Func<Vector3, Color> Gradient(Color sky, Color equator, Color ground) => d =>
    d.y >= 0 ? Color.Lerp(equator, sky, Mathf.Pow(d.y, 0.6f))
             : Color.Lerp(equator, ground, Mathf.Pow(-d.y, 0.6f));

// 使用:
RenderSettings.ambientProbe = EnvironmentTools.ProjectToSH(
    EnvironmentTools.Gradient(skyColor, equatorColor, groundColor));
图 8-2同样的主光,不同的环境光。左:Color(常数灰色,各方向一样,球体背光面平淡);中:Gradient(蓝色天空 + 暖色地面,球体底部被地面“反弹”成暖色);右:Skybox(拍摄环境的程序化天空投影成 SH)。物体使用本章案例 8.9 的 GILit。

注意中图球体下半部分的暖色——这是 Gradient 环境里 Ground 颜色的贡献,它模拟了“地面反弹光”。Color 模式下物体的立体感完全来自主光,背光面是一片均匀的灰,这就是为什么老游戏里的背光面总显得“假”。