GameTech/URP 渲染实战

案例 11.5预滤波环境图与 mip 选择进阶

Assets/Cookbook/Ch11/MyPBR_Lighting.hlsl(节选:env_sample)
// 取预滤波环境图(split-sum 的第一项)
float3 MySamplePrefilteredEnv(float3 R, float perceptualRoughness, float3 positionWS, float2 screenUV)
{
#if USE_FORWARD_PLUS || defined(_REFLECTION_PROBE_BLENDING)
    // Forward+ 把所有反射探针打包进一张八面体图集,天空反射走 _GlossyEnvironmentCubeMap;
    // 探针混合也有一套权重逻辑。这些都是“取样”,与 BRDF 无关,直接复用 URP 的取样函数
    // (它内部同样用感知粗糙度选 mip,映射公式与 MyRoughnessToMip 相同)。
    return GlossyEnvironmentReflection(R, positionWS, perceptualRoughness, 1.0h, screenUV);
#else
    #if defined(_REFLECTION_PROBE_BOX_PROJECTION)
    R = BoxProjectedCubemapDirection(R, positionWS, unity_SpecCube0_ProbePosition,
                                     unity_SpecCube0_BoxMin, unity_SpecCube0_BoxMax);
    #endif
    float mip = MyRoughnessToMip(perceptualRoughness);
    half4 encoded = SAMPLE_TEXTURECUBE_LOD(unity_SpecCube0, samplerunity_SpecCube0, R, mip);
    return DecodeHDREnvironment(encoded, unity_SpecCube0_HDR);   // 解 RGBM / HDR 编码
#endif
}
// 取预滤波环境图(split-sum 的第一项)
// HLSL 版在 Forward+ / 探针混合时改用 URP 的 GlossyEnvironmentReflection(反射探针图集);
// 网页版只有一个反射立方体图 unity_SpecCube0,两条路径结果相同,这里只保留手写的那条。
vec3 MySamplePrefilteredEnv(vec3 R, float perceptualRoughness, vec3 positionWS, vec2 screenUV)
{
    float mip = MyRoughnessToMip(perceptualRoughness);
    vec4 encoded = textureLod(unity_SpecCube0, R, mip);
    return DecodeHDREnvironment(encoded, unity_SpecCube0_HDR);   // 解 RGBM / HDR 编码
}

Unity 在烘焙反射探针、或者从天空盒生成默认反射时,会对 cubemap 做模拟镜面波瓣、按 mip 级递增模糊的卷积(具体滤波核以引擎实现为准),结果存在 mip 链里(core 包把最后一级的序号定义为 UNITY_SPECCUBE_LOD_STEPS = 6)。我们要做的就是把感知粗糙度映射到 mip 级:

mip=r(1.7−0.7r)×6

为什么不是线性的 r×6?因为 GGX 波瓣的张角与粗糙度不是线性关系:低粗糙度区间里,波瓣宽度变化很快,需要更多的 mip 级来区分;高粗糙度时波瓣已经很宽,几级 mip 差别不大。r(1.7−0.7r) 是一条在 r=0 时斜率为 1.7、r=1 时恰好为 1 的抛物线,把更多的 mip 分配给光滑端。这个映射与 URP 的 PerceptualRoughnessToMipmapLevel 相同,我们自己写一遍是为了看清它,同时保证和 URP Lit 采样同一级 mip,便于对比。

#if 的第一个分支是本章唯一“复用 URP 取样逻辑”的地方,值得解释为什么:Forward+ 下 URP 不再给每个物体绑定 unity_SpecCube0/1,而是把场景里所有反射探针的各级 mip 打包进一张八面体映射的二维图集,再加上分簇查找和按体积/重要性的权重混合;剩余权重落到天空反射 _GlossyEnvironmentCubeMap。开启 Reflection Probe Blending 时 Forward 路径也有一套双探针权重逻辑。这些都是“去哪里取环境光”的问题,和 BRDF 无关,自己重写只会制造与 URP 不一致的风险。第二个分支是传统 Forward 的单探针路径,我们手写了 mip 选择、盒体投影和 HDR 解码,作为理解 split-sum 左半的最小实现。

深入严格来说,split-sum 左半的预滤波应该按 GGX 波瓣做重要性采样卷积,并且卷积时还要乘 NoL 作权重(这正是公式里分子分母都有 n·l 的原因)。本书配图脚本里的程序化天空 cubemap 只做了普通的盒式降采样 mip,并非严格卷积,所以配图中粗糙球的反射模糊程度只具有示意意义。项目中请使用烘焙或实时更新的反射探针。