GameTech/URP 渲染实战

案例 24.1Enum 与 Boolean Keyword入门

目标:一张图里放三种程序化图案(条纹、圆点、棋盘),用 Enum Keyword 在材质上切换;再用一个 Boolean Keyword 控制是否叠加边缘光。

  1. Blackboard 添加 Keyword › Enum,命名 Pattern,Reference 改为 _PATTERN;Entries 里添加三项,Reference 分别是 STRIPES、DOTS、CHECKER;Definition 选 Shader Feature,Scope 选 Local。
  2. 添加 Keyword › Boolean,命名 Use Rim,Reference _USE_RIM_ON,Definition Shader Feature,Scope Local。
  3. 把 Pattern 拖进工作区,它有 Stripes、Dots、Checker 三个输入口:分别接三种图案的计算结果(UV × Tiling 后用 Fraction、Step、Distance 等节点算出 0/1 遮罩),输出接 Lerp 的 T,在两种颜色之间插值。
  4. Lerp 结果乘上光照后接到 Use Rim 节点的 Off 口;同一结果加上 Fresnel Effect × 颜色后接 On 口;Use Rim 的输出连 Base Color。

这张图生成的属性和 pragma,与下面这段手写代码逐字相同:

// Properties
[KeywordEnum(Stripes, Dots, Checker)] _PATTERN ("Pattern", Float) = 0
[Toggle] _USE_RIM ("Use Rim", Float) = 0
Assets/Cookbook/Ch24/SGKeywordEquivalent.shader(节选:pragmas)
#pragma shader_feature_local _PATTERN_STRIPES _PATTERN_DOTS _PATTERN_CHECKER
#pragma shader_feature_local _ _USE_RIM_ON
#pragma shader_feature_local _PATTERN_STRIPES _PATTERN_DOTS _PATTERN_CHECKER
#pragma shader_feature_local _ _USE_RIM_ON

Keyword 节点在生成的 Surface Description 函数里就是预处理分支:

Assets/Cookbook/Ch24/SGKeywordEquivalent.shader(节选:keyword)
half4 frag(Varyings i) : SV_Target
{
    float2 uv = i.uv * _Tiling;
    half mask;
    // 一个 Enum Keyword 节点 = 一组 #if/#elif,每个分支对应节点上的一个输入口
#if defined(_PATTERN_STRIPES)
    mask = step(0.5, frac(uv.x + uv.y));
#elif defined(_PATTERN_DOTS)
    mask = step(length(frac(uv) - 0.5), 0.3);
#else // _PATTERN_CHECKER
    float2 c = floor(uv);
    mask = fmod(c.x + c.y, 2.0);
#endif
    half3 albedo = lerp(_ColorA.rgb, _ColorB.rgb, mask);

    half3 n = normalize(i.normalWS);
    Light l = GetMainLight();
    half3 color = albedo * (l.color * saturate(dot(n, l.direction)) + SampleSH(n));

    // Boolean Keyword 节点 = 一个 #if,On / Off 两个输入口
#if defined(_USE_RIM_ON)
    half3 v = GetWorldSpaceNormalizeViewDir(i.positionWS);
    color += pow(1 - saturate(dot(n, v)), 3) * half3(1.0, 0.9, 0.7) * 1.5;
#endif
    return half4(color, 1);
}
vec4 frag()
{
    vec2 uv = uv0 * _Tiling;
    float mask;
    // 一个 Enum Keyword 节点 = 一组 #if/#elif,每个分支对应节点上的一个输入口
#if defined(_PATTERN_STRIPES)
    mask = step(0.5, fract(uv.x + uv.y));
#elif defined(_PATTERN_DOTS)
    mask = step(length(fract(uv) - 0.5), 0.3);
#else // _PATTERN_CHECKER
    vec2 c = floor(uv);
    float s = c.x + c.y;
    mask = s - 2.0 * trunc(s / 2.0);   // HLSL 的 fmod(s, 2)(负数时结果为负,与 GLSL 的 mod 不同)
#endif
    vec3 albedo = mix(_ColorA.rgb, _ColorB.rgb, mask);

    vec3 n = normalize(normalWS);
    Light l = GetMainLight();
    vec3 color = albedo * (l.color * saturate(dot(n, l.direction)) + SampleSH(n));

    // Boolean Keyword 节点 = 一个 #if,On / Off 两个输入口
#if defined(_USE_RIM_ON)
    vec3 v = GetWorldSpaceNormalizeViewDir(positionWS);
    color += pow(1.0 - saturate(dot(n, v)), 3.0) * vec3(1.0, 0.9, 0.7) * 1.5;
#endif
    return vec4(color, 1);
}
图 24-2同一张图的三个变体(由等价手写 Shader 渲染)。左:_PATTERN_STRIPES;中:_PATTERN_DOTS;右:_PATTERN_CHECKER + _USE_RIM_ON

变体数:这张图在这两个 Keyword 上产生 3 × 2 = 6 个变体,再乘以 URP Target 自带的那些 multi_compile(阴影、光照贴图、Forward+、雾……)。Keyword 是变体爆炸的主要来源,几条经验:只在材质上切换的开关用 Shader Feature + Local,未被任何材质使用的 shader_feature 变体打包时会被剔除;只影响片元的开关把 Stages 设为 Fragment;能用一个 Float 属性 + 分支(或 Lerp)解决、分支两侧开销又不大的,就不要用 Keyword。

注意用 C# 代码创建或修改材质时,KeywordEnum 的切换要手动处理关键字:先 DisableKeyword 同组的其他关键字,再 EnableKeyword 目标关键字。只改 Float 属性值不会同步关键字(材质 Inspector 会替你同步,脚本不会);同组多个关键字同时开着时,选中哪个变体是不确定的。本书的配图脚本就踩过这个坑。