URP 渲染实战 / 第 14 章 卡通渲染 / 案例 14.4
案例 14.4风格化高光:圆斑、色块、方块进阶
目标:把高光做成一块硬边色块,并通过平移、缩放、分裂、方形化来控制它的形状。
最基本的卡通高光就是对 Blinn-Phong 的 N·H 做色阶:ToonBand(N·H, 0.97, ...),得到一块硬边的圆形。但卡通美术常常想要更有设计感的形状:拉长的、偏到一边的、方形的(像窗户的反光)、分成两三块的。Anjyo 和 Hiramitsu 在 2003 年的论文《Stylized Highlights for Cartoon Rendering》里给出了一套方法:既然高光的形状由“H 离 N 有多近”决定,那就在切线空间里直接对 H 动手脚。
- 平移:,高光整体挪动。
- 方向缩放:,削弱 H 在 T 方向的分量,高光在 T 方向被拉长。
- 分裂:,T 方向的两侧各往外推,一个高光分成两块(T、B 同时分裂则成四块)。
- 方形化:H 在切平面上的分量 (x, y) = (H·T, H·B) 的长度就是 ,“N·H ≥ 阈值”等价于“这个长度 ≤ sin(阈值角)”。把长度(L2 范数)换成 Lp 范数 ,p = 2 是圆,p 越大越接近正方形。原论文的方形化用的是另一个基于角度的公式,在阈值较高时形状控制不够直接,这里换成了等价思路下更好调的 Lp 范数写法。
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)); }

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