GameTech/URP 渲染实战

案例 12.8油膜与彩虹色高阶

目标:用干涉反射率替换 GGX 里的 Schlick 菲涅尔,做出油膜效果;对比 3 波长、16 波长光谱积分,以及 core 包里 HDRP 使用的 Belcour 实现。

Assets/Cookbook/Ch12/ThinFilm.shader(节选:film)
float3 FilmFresnel(float cosTheta, float thickness)
{
#if defined(_FILM_RGB3)
    // 偷懒版:只在 R/G/B 三个代表波长上算一次 Airy 反射率
    float sin2 = (1.0 - cosTheta * cosTheta) / (_FilmIOR * _FilmIOR);
    float opd = 2.0 * _FilmIOR * thickness * sqrt(saturate(1.0 - sin2));
    float r12 = -sqrt(AdvF_Schlick(Sq((1.0 - _FilmIOR) / (1.0 + _FilmIOR)), 1.0, cosTheta));
    float baseIOR = (1.0 + sqrt(_BaseF0)) / (1.0 - sqrt(min(_BaseF0, 0.99)));
    float r23 = sign(_FilmIOR - baseIOR) * sqrt(Sq((_FilmIOR - baseIOR) / (_FilmIOR + baseIOR)));
    return float3(AdvAiryReflectance(r12, r23, opd, 650.0),
                  AdvAiryReflectance(r12, r23, opd, 532.0),
                  AdvAiryReflectance(r12, r23, opd, 450.0));
#elif defined(_FILM_BELCOUR)
    // core 包里 HDRP 用的 Belcour & Barla 2017 实现;它的厚度参数是 0~1 的归一化值
    return EvalIridescence(1.0, cosTheta, saturate(thickness / 1000.0), _BaseF0.xxx);
#else
    return AdvThinFilm(cosTheta, thickness, _FilmIOR, _BaseF0);
#endif
}
vec3 FilmFresnel(float cosTheta, float thickness)
{
#if defined(_FILM_RGB3)
    // 偷懒版:只在 R/G/B 三个代表波长上算一次 Airy 反射率
    float sin2 = (1.0 - cosTheta * cosTheta) / (_FilmIOR * _FilmIOR);
    float opd = 2.0 * _FilmIOR * thickness * sqrt(saturate(1.0 - sin2));
    float r12 = -sqrt(AdvF_Schlick(Sq((1.0 - _FilmIOR) / (1.0 + _FilmIOR)), 1.0, cosTheta));
    float baseIOR = (1.0 + sqrt(_BaseF0)) / (1.0 - sqrt(min(_BaseF0, 0.99)));
    float r23 = sign(_FilmIOR - baseIOR) * sqrt(Sq((_FilmIOR - baseIOR) / (_FilmIOR + baseIOR)));
    return vec3(AdvAiryReflectance(r12, r23, opd, 650.0),
                AdvAiryReflectance(r12, r23, opd, 532.0),
                AdvAiryReflectance(r12, r23, opd, 450.0));
#elif defined(_FILM_BELCOUR)
    // core 包里 HDRP 用的 Belcour & Barla 2017 实现;它的厚度参数是 0~1 的归一化值
    return EvalIridescence(1.0, cosTheta, saturate(thickness / 1000.0), vec3(_BaseF0));
#else
    return AdvThinFilm(cosTheta, thickness, _FilmIOR, _BaseF0);
#endif
}

core 包的 BSDF.hlsl 里有一个现成的 EvalIridescence(Belcour 与 Barla 2017 的实现,HDRP 的 Iridescence 材质就用它),它在傅里叶空间里对配色函数做了解析积分,比逐波长采样更高效、也更准确。它的厚度参数是一个 0~1 的归一化值,内部还用它同时推导出膜的折射率,所以和我们的“纳米 + 折射率”参数不能一一对应,对比时只看颜色的“风格”即可。因为 URP 的 Lighting.hlsl 已经间接包含了 BSDF.hlsl,在 URP Shader 里可以直接调用它。

膜厚加了一张噪声贴图做空间变化:真实的油膜和肥皂泡厚度从来不均匀,正是厚度的起伏造成了一圈圈的彩色条纹。

图 12-24膜厚从左到右 150、250、350、450、550、650 nm(膜折射率 1.6,基底 F0 = 0.3)。同一个球上,中心与边缘的颜色也不同:视角越斜,膜内的光程差越小
图 12-25膜厚 450 ± 250 nm 的油膜纽结。左:16 波长光谱积分;中:只算 3 个代表波长,颜色偏紫绿、过渡生硬;右:core 包的 EvalIridescence(Belcour 2017)

膜厚和颜色的对应有一个经验规律:100 nm 以下几乎没有颜色(只是略微变暗或变亮);200~600 nm 是一阶、二阶干涉色,颜色最鲜艳;超过 1 μm 后,相邻波长的相位差太大,各种颜色互相抵消,又趋于灰白。所以想要“鲜艳的彩虹色”,膜厚的变化范围要落在几百纳米内。

调参建议基底越暗,干涉色越显眼:肥皂泡之所以五彩斑斓,是因为它背后是透出来的环境,自身几乎没有漫反射。做甲虫壳、改装车的“变色龙漆”时,可以把薄膜干涉放在清漆层上(把清漆的 F 换成干涉反射率),底层用深色金属——这就是把 12.2 节和本节组合起来。