GameTech/URP 渲染实战

案例 14.9卡通化的接收阴影进阶

目标:让物体接收的投影也符合卡通风格:硬边、使用阴影色,而且与自身的明暗交界协调。

URP 的阴影采样返回的是 0~1 之间的软阴影值(PCF 滤波的结果)。最省事的做法是把它乘到最终颜色上,但在卡通画面里这会出现三个问题:阴影边缘是一条模糊的灰带,和硬边的色阶格格不入;阴影里的颜色是“固有色变暗”而不是指定的阴影色;投影区域和自身背光区域是两种不同的暗,画面里出现第三种颜色。

正确的做法是把阴影“折”进色阶的输入里:先对阴影值做一次色阶变成硬边,再用它去压低半 Lambert,然后统一走色阶 / Ramp 流程。这样投影和背光用的是同一套阴影色。还有一个细节:被投影的区域只应该压到“第一层阴影”的位置,而不是压到 0——压到 0 会掉进第二层阴影,比背光面还暗,看起来像是“阴影里的阴影”。

Assets/Cookbook/Ch14/ToonLit.shader(节选:diffuse)
// 主光的色阶:输入是“半 Lambert × 卡通化后的阴影”,输出是最终的漫反射颜色
half3 ToonDiffuse(half3 albedo, half halfLambert, half shadow)
{
    // 接收阴影卡通化:把 PCF 的软阴影值也切成硬边,再“乘进”明暗,而不是在最后乘一次
    half toonShadow = lerp(1.0, ToonBand(shadow, 0.5, _ShadowSoftness), _ReceiveShadow);
    // 投影区域只压到“第一层阴影”的位置,而不是压到 0(那会掉进第二层阴影,比背光面还暗)
    half shadowedX = min(halfLambert, (_Threshold + _Shade2Threshold) * 0.5);
    half x = lerp(shadowedX, halfLambert, toonShadow);
#if defined(_RAMPMAP)
    return albedo * SAMPLE_TEXTURE2D(_RampTex, sampler_RampTex, float2(clamp(x, 0.01, 0.99), 0.5)).rgb;
#else
    half band1 = ToonBand(x, _Threshold, _Softness);
    half band2 = ToonBand(x, _Shade2Threshold, _Softness);
    half3 shade = lerp(albedo * _ShadeColor2.rgb, albedo * _ShadeColor.rgb, lerp(1.0, band2, _Shade2Strength));
    return lerp(shade, albedo, band1);
#endif
}
// 主光的色阶:输入是“半 Lambert × 卡通化后的阴影”,输出是最终的漫反射颜色
vec3 ToonDiffuse(vec3 albedo, float halfLambert, float shadow)
{
    // 接收阴影卡通化:把 PCF 的软阴影值也切成硬边,再“乘进”明暗,而不是在最后乘一次
    float toonShadow = mix(1.0, ToonBand(shadow, 0.5, _ShadowSoftness), _ReceiveShadow);
    // 投影区域只压到“第一层阴影”的位置,而不是压到 0(那会掉进第二层阴影,比背光面还暗)
    float shadowedX = min(halfLambert, (_Threshold + _Shade2Threshold) * 0.5);
    float x = mix(shadowedX, halfLambert, toonShadow);
#if defined(_RAMPMAP)
    return albedo * texture(_RampTex, vec2(clamp(x, 0.01, 0.99), 0.5)).rgb;
#else
    float band1 = ToonBand(x, _Threshold, _Softness);
    float band2 = ToonBand(x, _Shade2Threshold, _Softness);
    vec3 shade = mix(albedo * _ShadeColor2.rgb, albedo * _ShadeColor.rgb, mix(1.0, band2, _Shade2Strength));
    return mix(shade, albedo, band1);
#endif
}
图 14-18柱子的影子投在胶囊上(为了看清阴影边缘,阴影图分辨率特意降到 512)。左:不接收阴影;中:接收阴影但不卡通化,影子边缘是一条模糊的过渡带;右:卡通化后,影子与自身的背光面使用同一种阴影色,边缘硬朗

卡通化之后,阴影图的走样会变得更显眼:原本被 PCF 模糊掉的锯齿,被色阶重新“锐化”了出来,在明暗交界线附近还可能出现自阴影的瑕疵(shadow acne)。常用的缓解办法有: