GameTech/URP 渲染实战

案例 5.2Half-Lambert:让暗部不再死黑入门

目标:理解 Valve 在《半条命 2》中提出的 Half-Lambert 光照,用它让角色背光面保留形体。

原理

Lambert 的问题在于:从交界线开始,整个背光的半球都是同一个值 0。没有环境光时背光面一片死黑;即使有环境光,背光面也只剩下环境光那一点点起伏,形体信息丢失严重。Valve 的做法非常直接:不截断 N·L,而是把它的取值范围 [−1, 1] 线性地映射到 [0, 1],再取平方:

Dhalf=(0.5⋅N⋅L+0.5)2

映射以后,正对光源仍是 1,交界线处变成 0.5(平方后 0.25),只有正背面才是 0。背光面从“一片 0”变成了一个平滑的渐变,形体就回来了。平方的作用是把整体拉暗一点、让亮暗过渡更接近 Lambert 的观感,否则映射后的结果会显得发灰、发平。下图把几种漫反射曲线画在一起,横轴是法线与光线的夹角 θ。

交界线 θ = 90° 00.51 0°45°90°135°180° 法线与光线的夹角 θ Lambert Half-Lambert Half-Lambert² Wrap (w = 0.5)
图 5-6几种漫反射曲线。Lambert 在 90° 之后恒为 0;Half-Lambert 一直延伸到 180°;Wrap 介于两者之间,在 θ = arccos(−w) 处归零

代码

Half-Lambert 的 Shader 只有片元函数和 Lambert 不同,这里只列出这一段:

Assets/Cookbook/Ch05/HalfLambert.shader(节选:frag)
half4 frag(Varyings input) : SV_Target
{
    half3 N = normalize(input.normalWS);
    Light mainLight = GetMainLight();
    half3 radiance = mainLight.color * mainLight.distanceAttenuation;

    // 注意这里没有 saturate:背光面的负值也要参与映射
    half NdotL = dot(N, mainLight.direction);
    half halfLambert = NdotL * 0.5 + 0.5;          // [-1,1] -> [0,1]
    halfLambert = pow(halfLambert, _Power);         // Valve 原版取平方,让暗部重新“沉下去”

    half3 diffuse = _BaseColor.rgb * radiance * halfLambert;
    half3 ambient = _BaseColor.rgb * SampleSH(N) * _AmbientStrength;
    return half4(diffuse + ambient, 1);
}
void main()
{
    vec3 N = normalize(normalWS);
    Light mainLight = GetMainLight();
    vec3 radiance = mainLight.color * mainLight.distanceAttenuation;

    // 注意这里没有 saturate:背光面的负值也要参与映射
    float NdotL = dot(N, mainLight.direction);
    float halfLambert = NdotL * 0.5 + 0.5;          // [-1,1] -> [0,1]
    halfLambert = pow(halfLambert, _Power);         // Valve 原版取平方,让暗部重新“沉下去”

    vec3 diffuse = _BaseColor.rgb * radiance * halfLambert;
    vec3 ambient = _BaseColor.rgb * SampleSH(N) * _AmbientStrength;
    fragColor = vec4(diffuse + ambient, 1.0);
}

和 Lambert 的唯一区别是:先映射再截断,所以 dot 的结果不能 saturate。_Power 设为 1 是不平方的版本,设为 2 是 Valve 原版。

图 5-7同一个球、同一盏侧光、都没有环境光。左:Lambert,背光半球全黑;中:Half-Lambert(不平方),背光面明显发灰、立体感弱;右:Half-Lambert²,背光面保留了渐变,整体对比更接近真实

Half-Lambert 在物理上没有依据:它让背光面也“收到”了光,能量凭空多出来一截。它真正模拟的,是“环境里还有很多来自背后的反弹光”这件事——在没有像样的全局光照的年代,这是一个便宜又好看的折中。今天它主要用于:

坑Half-Lambert 和实时阴影放在一起时容易“穿帮”:阴影图判断的是几何遮挡,它的边界在 N·L = 0 附近;而 Half-Lambert 在 N·L = 0 处仍有 0.25 的亮度。于是物体自身投影的边缘会出现一条突兀的硬线。常见的处理是让阴影只作用于 Lambert 部分,或者把阴影衰减也做同样的“柔化”。第 14 章的卡通 Shader 会具体处理这个问题。