URP 渲染实战 / 第 11 章 从零手写 PBR / 案例 11.5
案例 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 级:
为什么不是线性的 ?因为 GGX 波瓣的张角与粗糙度不是线性关系:低粗糙度区间里,波瓣宽度变化很快,需要更多的 mip 级来区分;高粗糙度时波瓣已经很宽,几级 mip 差别不大。 是一条在 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,并非严格卷积,所以配图中粗糙球的反射模糊程度只具有示意意义。项目中请使用烘焙或实时更新的反射探针。