URP 渲染实战 / 第 20 章 地形与植被 / 案例 20.4
案例 20.4高度/坡度自动材质:草 → 砂石 → 岩石 → 雪进阶
目标:一个 Shader 自动给整块地形分配四种地表,并且层间过渡自然。
四张地表贴图都是拍摄时用 C# 程序生成的可平铺噪声(见 Ch20/Editor/TerrainTextures.cs),RGB 是颜色,A 通道存“高度”:草叶尖高、根部低;岩石的板块高、裂缝低;卵石中心高、砂粒低。高度信息就是后面高度混合的依据。
Assets/Cookbook/Ch20/AutoTerrain.shader(节选:rules)
half4 frag(Varyings i) : SV_Target { half3 n = normalize(i.normalWS); float3 pos = i.positionWS; // 低频噪声:打破“等高线”一样整齐的分界 half noise = SAMPLE_TEXTURE2D(_NoiseTex, sampler_NoiseTex, pos.xz * 0.04).r - 0.5; half h = pos.y + noise * _NoiseAmp; half slope = 1.0 - n.y; // 0 = 平地,1 = 垂直崖壁 // 平缓的层用顶视投影即可;岩石主要出现在陡坡,必须 Triplanar float2 uvTop = pos.xz * _Tiling; half4 grass = SAMPLE_TEXTURE2D(_GrassTex, sampler_GrassTex, uvTop); half4 sand = SAMPLE_TEXTURE2D(_SandTex, sampler_GrassTex, uvTop * 1.3); half4 snow = SAMPLE_TEXTURE2D(_SnowTex, sampler_GrassTex, uvTop * 0.5); float3 tw = TriplanarWeights(n, 8); TriplanarUV tuv = GetTriplanarUV(pos, n, _RockTiling); half4 rock = SampleTriplanar(TEXTURE2D_ARGS(_RockTex, sampler_GrassTex), tuv, tw); half3 rockN = TriplanarNormalWS(TEXTURE2D_ARGS(_RockNormal, sampler_GrassTex), tuv, tw, n, 1, 3); // 规则 → 遮罩(0..1) half sandMask = 1.0 - smoothstep(_SandHeight - _HeightBand, _SandHeight + _HeightBand, h); half rockMask = smoothstep(_RockSlope - _SlopeBand, _RockSlope + _SlopeBand, slope + noise * 0.25);
vec4 frag() { vec3 n = normalize(normalWS); vec3 pos = positionWS; // 低频噪声:打破“等高线”一样整齐的分界 float noise = texture(_NoiseTex, pos.xz * 0.04).r - 0.5; float h = pos.y + noise * _NoiseAmp; float slope = 1.0 - n.y; // 0 = 平地,1 = 垂直崖壁 // 平缓的层用顶视投影即可;岩石主要出现在陡坡,必须 Triplanar vec2 uvTop = pos.xz * _Tiling; vec4 grass = texture(_GrassTex, uvTop); vec4 sand = texture(_SandTex, uvTop * 1.3); vec4 snow = texture(_SnowTex, uvTop * 0.5); vec3 tw = TriplanarWeights(n, 8.0); TriplanarUV tuv = GetTriplanarUV(pos, n, _RockTiling); vec4 rock = SampleTriplanar(_RockTex, tuv, tw); vec3 rockN = TriplanarNormalWS(_RockNormal, tuv, tw, n, 1.0, 3); // 规则 → 遮罩(0..1) float sandMask = 1.0 - smoothstep(_SandHeight - _HeightBand, _SandHeight + _HeightBand, h); float rockMask = smoothstep(_RockSlope - _SlopeBand, _RockSlope + _SlopeBand, slope + noise * 0.25);
这里有几个细节:
- 噪声同时扰动高度和坡度。同一份低频噪声
noise加到高度判断上(乘_NoiseAmp米),也加到坡度判断上(乘 0.25),雪线和岩石边界就都不再是光滑曲线。 - 只有岩石做 Triplanar。草、砂、雪只出现在平缓处,用顶视投影一次采样足够;岩石专门出现在陡坡,必须三平面。这样整个 Shader 的采样数从“每层 3 次”降到“1+1+3+3(法线)+1+1(噪声)”。
- 共用采样器。五张图都用
sampler_GrassTex采样。在部分移动平台和图形 API 上,一个 Shader 能用的采样器(Sampler State)数量有限(常见上限是 16),地形 Shader 层数一多很容易超,URP 的 TerrainLit 也是所有层共用sampler_Splat0。
遮罩算好之后,按“从下往上铺”的顺序逐层叠加:
Assets/Cookbook/Ch20/AutoTerrain.shader(节选:stack)
// 按“从下往上铺”的顺序逐层叠:每层都和“已经铺好的结果”做一次高度混合 half4 c = grass; half3 nrm = n; half4 layerW = half4(1, 0, 0, 0); // 记录各层最终占比:草/砂/岩/雪(调试用) half t = HeightBlendWeight(c.a, sand.a, sandMask, _BlendDepth); c = lerp(c, sand, t); layerW = half4(layerW.xyz * (1 - t), 0); layerW.y += t; t = HeightBlendWeight(c.a, rock.a, rockMask, _BlendDepth); c = lerp(c, rock, t); nrm = normalize(lerp(nrm, rockN, t)); layerW.xyz *= (1 - t); layerW.z += t; // 雪:高于雪线,并且“细节法线”朝上的地方才积得住——岩石的凹坑与台面先白 half snowSlope = 1.0 - nrm.y; half snowMask = smoothstep(_SnowHeight - _HeightBand, _SnowHeight + _HeightBand, h) * (1.0 - smoothstep(0.35, 0.65, snowSlope)); t = HeightBlendWeight(c.a, snow.a, snowMask, _BlendDepth); c = lerp(c, snow, t); nrm = normalize(lerp(nrm, n, t * 0.6)); layerW *= (1 - t); layerW.w += t;
// 按“从下往上铺”的顺序逐层叠:每层都和“已经铺好的结果”做一次高度混合 vec4 c = grass; vec3 nrm = n; vec4 layerW = vec4(1, 0, 0, 0); // 记录各层最终占比:草/砂/岩/雪(调试用) float t = HeightBlendWeight(c.a, sand.a, sandMask, _BlendDepth); c = mix(c, sand, t); layerW = vec4(layerW.xyz * (1.0 - t), 0.0); layerW.y += t; t = HeightBlendWeight(c.a, rock.a, rockMask, _BlendDepth); c = mix(c, rock, t); nrm = normalize(mix(nrm, rockN, t)); layerW.xyz *= (1.0 - t); layerW.z += t; // 雪:高于雪线,并且“细节法线”朝上的地方才积得住——岩石的凹坑与台面先白 float snowSlope = 1.0 - nrm.y; float snowMask = smoothstep(_SnowHeight - _HeightBand, _SnowHeight + _HeightBand, h) * (1.0 - smoothstep(0.35, 0.65, snowSlope)); t = HeightBlendWeight(c.a, snow.a, snowMask, _BlendDepth); c = mix(c, snow, t); nrm = normalize(mix(nrm, n, t * 0.6)); layerW *= (1.0 - t); layerW.w += t;
每叠一层,都拿“已经铺好的结果”(包括它的高度 c.a)和新层做一次高度混合;同时把各层最终占比记录在 layerW 里,既用来混合光滑度,也用于调试显示。雪的规则特意用了叠加岩石法线之后的坡度:岩石凹坑和台面朝上的细节先积雪,竖直的岩面保持裸露,这比用几何法线判断要自然得多。


调参顺序先打开调试视图,只看色块分布:调
_RockSlope 决定多陡算崖壁、_SnowHeight 决定雪线、_NoiseAmp 决定边界的破碎程度;分布满意后再切回正常视图调 _BlendDepth。分布和过渡是两件事,混在一起调很容易顾此失彼。本案例出自《URP 渲染实战》第 20 章「地形与植被」。在章节中阅读上下文 →