GameTech/URP 渲染实战

案例 14.4风格化高光:圆斑、色块、方块进阶

目标:把高光做成一块硬边色块,并通过平移、缩放、分裂、方形化来控制它的形状。

最基本的卡通高光就是对 Blinn-Phong 的 N·H 做色阶:ToonBand(N·H, 0.97, ...),得到一块硬边的圆形。但卡通美术常常想要更有设计感的形状:拉长的、偏到一边的、方形的(像窗户的反光)、分成两三块的。Anjyo 和 Hiramitsu 在 2003 年的论文《Stylized Highlights for Cartoon Rendering》里给出了一套方法:既然高光的形状由“H 离 N 有多近”决定,那就在切线空间里直接对 H 动手脚。

Assets/Cookbook/Ch14/ToonCommon.hlsl(节选:spec)
// 风格化高光(思路来自 Anjyo & Hiramitsu 2003):在切线空间里对半角向量 H 做平移、缩放、分裂,
// 最后用“高光区域的形状”切出硬边。T/B 是表面的切线与副切线。
half StylizedSpecular(half3 N, half3 T, half3 B, half3 H, half4 shiftScale, half2 split, half square, half threshold, half softness)
{
    H = normalize(H + shiftScale.x * T + shiftScale.y * B);                 // 平移:整体挪动高光
    H = normalize(H - shiftScale.z * dot(H, T) * T - shiftScale.w * dot(H, B) * B);   // 方向缩放:拉长/压扁
    H = normalize(H - split.x * sign(dot(H, T)) * T - split.y * sign(dot(H, B)) * B); // 分裂:一分为二/四
    // 方形化:H 在切平面上的分量 (x, y) 的长度就是 sin(N,H)。“N·H ≥ 阈值”等价于“长度 ≤ sin(阈值角)”。
    // 把长度换成 Lp 范数,p = 2 是圆,p 越大越接近正方形
    half2 xy = abs(half2(dot(H, T), dot(H, B)));
    half p = lerp(2.0, 10.0, square);
    half d = pow(pow(xy.x, p) + pow(xy.y, p), 1.0 / p);
    half sinTh = sqrt(saturate(1.0 - threshold * threshold));
    return smoothstep(-softness, softness, sinTh - d) * step(0.0, dot(N, H));
}
// 风格化高光(思路来自 Anjyo & Hiramitsu 2003):在切线空间里对半角向量 H 做平移、缩放、分裂,
// 最后用“高光区域的形状”切出硬边。T/B 是表面的切线与副切线。
float StylizedSpecular(vec3 N, vec3 T, vec3 B, vec3 H, vec4 shiftScale, vec2 split, float square, float threshold, float softness)
{
    H = normalize(H + shiftScale.x * T + shiftScale.y * B);                 // 平移:整体挪动高光
    H = normalize(H - shiftScale.z * dot(H, T) * T - shiftScale.w * dot(H, B) * B);   // 方向缩放:拉长/压扁
    H = normalize(H - split.x * sign(dot(H, T)) * T - split.y * sign(dot(H, B)) * B); // 分裂:一分为二/四
    // 方形化:H 在切平面上的分量 (x, y) 的长度就是 sin(N,H)。“N·H ≥ 阈值”等价于“长度 ≤ sin(阈值角)”。
    // 把长度换成 Lp 范数,p = 2 是圆,p 越大越接近正方形
    vec2 xy = abs(vec2(dot(H, T), dot(H, B)));
    float p = mix(2.0, 10.0, square);
    float d = pow(pow(xy.x, p) + pow(xy.y, p), 1.0 / p);
    float sinTh = sqrt(saturate(1.0 - threshold * threshold));
    return smoothstep(-softness, softness, sinTh - d) * step(0.0, dot(N, H));
}
图 14-8从左到右:Blinn-Phong 原始高光;色阶化的硬边圆斑;平移 + 沿切线方向缩放,变成横向的长条;分裂 + 方形化,变成“田”字形的窗户反光

这些变换都依赖切线 T、B 的方向,所以和第 12 章的各向异性一样,需要模型有合理的切线。对角色来说,通常在头发和眼睛上用得最多(眼睛的高光几乎都是贴图画的,但头发、金属饰品、头盔上的风格化高光很适合用这种方法)。