GameTech/URP 渲染实战

案例 8.49 个系数到底有多准进阶

目标:选一个高对比度的“摄影棚”环境(深灰背景 + 两块强烈的柔光箱 + 一条暖色轮廓灯),用暴力积分算出真实的辐照度,与 SH 近似并排比较。

“标准答案”通过对每个法线方向做余弦加权的半球积分得到:在半球上按余弦分布采样方向,概率密度 cosθ/π 正好与被积函数中的 cosθ/π 抵消,积分就变成了样本的简单平均。我们把结果存进一张小 Cubemap:

Assets/Cookbook/Ch08/EnvironmentTools.cs(节选:irradiance)
/// “标准答案”:对每个法线方向做余弦加权的半球积分,得到 URP 约定下的间接漫反射(E/π)。
/// 用来和 9 个系数的 SH 近似做对比。
public static Cubemap BakeIrradiance(Func<Vector3, Color> radiance, int size = 32, int samples = 1024)
{
    var cube = new Cubemap(size, TextureFormat.RGBAHalf, false) { name = "Ch08Irradiance", hideFlags = HideFlags.DontSave };
    var px = new Color[size * size];
    for (int f = 0; f < 6; f++)
    {
        for (int y = 0; y < size; y++)
            for (int x = 0; x < size; x++)
            {
                Vector3 n = FaceDirection((CubemapFace)f, (x + 0.5f) / size * 2f - 1f, (y + 0.5f) / size * 2f - 1f);
                Vector3 up = Mathf.Abs(n.y) < 0.999f ? Vector3.up : Vector3.right;
                Vector3 tx = Vector3.Normalize(Vector3.Cross(up, n)), ty = Vector3.Cross(n, tx);
                Color sum = Color.black;
                int sq = Mathf.RoundToInt(Mathf.Sqrt(samples));
                for (int i = 0; i < sq; i++)
                    for (int j = 0; j < sq; j++)
                    {
                        // 分层的余弦加权采样:pdf = cosθ/π,正好与积分里的 cosθ/π 抵消,结果就是简单平均
                        float u1 = (i + 0.5f) / sq, u2 = (j + 0.5f) / sq;
                        float r = Mathf.Sqrt(u1), phi = 2f * Mathf.PI * u2;
                        Vector3 l = tx * (r * Mathf.Cos(phi)) + ty * (r * Mathf.Sin(phi)) + n * Mathf.Sqrt(1f - u1);
                        sum += radiance(l);
                    }
                px[y * size + x] = sum / (sq * sq);
            }
        cube.SetPixels(px, (CubemapFace)f);
    }
    cube.Apply(false, false);
    return cube;
}
图 8-6左:环境本身(按法线方向取环境颜色,曝光减半),可以看到锐利的柔光箱;中:对每个方向做 1024 次余弦加权采样得到的真实辐照度;右:同一环境投影成三阶 SH 后用 SampleSH 求值。中、右两图几乎一致。

左图里锐利的柔光箱在辐照度里变成了大面积的柔和渐变,而三阶 SH 准确地复现了这种渐变。SH 近似在什么时候会出问题?当环境里有一个极强的小光源(比如把太阳也算进环境光),截断到三阶会在光源的反方向产生“振铃”(Ringing),表现为背光面出现不该有的暗环甚至负值——SampleSH 最后的 max(0, …) 就是为了兜底。实践中的做法是把太阳交给方向光,环境光里只保留天空的漫射部分。本书拍摄环境的默认天空在投影 SH 时就把太阳尖峰截掉了。

光照探针

环境光 SH 对整个场景只有一组,室内和室外的物体拿到的环境光一样。光照探针(Light Probe)解决的是“空间上的变化”:在场景里摆放一组探针点(Light Probe Group),烘焙时每个点记录该位置的入射光(包括直接光和反弹光),同样压缩成三阶 SH。运行时,Unity 把所有探针点做四面体剖分,按动态物体的位置找到所在的四面体,用重心坐标插值四个探针的 SH,作为这个物体的 unity_SHAr 等变量。