URP 渲染实战 / 第 24 章 Shader Graph / 案例 24.1
案例 24.1Enum 与 Boolean Keyword入门
目标:一张图里放三种程序化图案(条纹、圆点、棋盘),用 Enum Keyword 在材质上切换;再用一个 Boolean Keyword 控制是否叠加边缘光。
- Blackboard 添加 Keyword › Enum,命名 Pattern,Reference 改为
_PATTERN;Entries 里添加三项,Reference 分别是STRIPES、DOTS、CHECKER;Definition 选 Shader Feature,Scope 选 Local。 - 添加 Keyword › Boolean,命名 Use Rim,Reference
_USE_RIM_ON,Definition Shader Feature,Scope Local。 - 把 Pattern 拖进工作区,它有 Stripes、Dots、Checker 三个输入口:分别接三种图案的计算结果(UV × Tiling 后用 Fraction、Step、Distance 等节点算出 0/1 遮罩),输出接 Lerp 的 T,在两种颜色之间插值。
- 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
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); }

_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 会替你同步,脚本不会);同组多个关键字同时开着时,选中哪个变体是不确定的。本书的配图脚本就踩过这个坑。