URP 渲染实战 / 第 14 章 卡通渲染 / 案例 14.9
案例 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 }

卡通化之后,阴影图的走样会变得更显眼:原本被 PCF 模糊掉的锯齿,被色阶重新“锐化”了出来,在明暗交界线附近还可能出现自阴影的瑕疵(shadow acne)。常用的缓解办法有:
- 在接收端把采样位置沿法线挪一点(
_ShadowNormalOffset),专门对付交界线附近的自阴影瑕疵。这与 URP 在投射端做的 Normal Bias 是互补的。 - 适当保留一点
_ShadowSoftness,让色阶边缘有一两个像素的过渡,而不是完全的 step。 - 对角色使用更高的阴影分辨率、更短的阴影距离或单独的角色阴影(第 23 章会讨论阴影参数的取舍)。
- 对于脸部这种对阴影形状极度敏感的地方,干脆不接收自阴影,只用 SDF 阴影。