URP 渲染实战 / 第 20 章 地形与植被 / 案例 20.1
案例 20.1世界空间 UV 与陡坡拉伸入门
目标:理解为什么地形不能直接用网格 UV,世界空间 XZ 投影又在哪里失效。
高度场网格的 UV 是在水平面上均匀分布的:第 i 列顶点的 u 就是 i/n。贴图因此是“从正上方投影”下来的,平地上完全没问题;但在一面 60° 的陡坡上,水平方向走 1 米,表面实际走了 2 米——同样的纹素被摊到了两倍的面积上,纹理被拉成竖条。更一般地,坡面与水平面夹角为 θ 时,沿坡方向的拉伸倍数是
其中 是单位法线的 y 分量。θ 接近 90° 时它趋于无穷,垂直的崖壁上只剩下一条条竖线。
世界空间 UV(World-Space UV)就是直接用 positionWS.xz × 平铺系数 当 UV。它与网格 UV 在平地上等价,但有两个好处:一是与网格分辨率、分块方式无关,多块地形拼接时纹理天然连续;二是平铺密度用“每米几次”来描述,美术更好控制。它解决不了陡坡拉伸——那需要下一个案例的三平面映射。下面这个对照 Shader 用一个 KeywordEnum 在三种贴法之间切换:
Assets/Cookbook/Ch20/UVCompare.shader(节选:frag)
half4 frag(Varyings i) : SV_Target { float3 n = normalize(i.normalWS); half3 albedo; #if defined(_UVMODE_MESHUV) albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.uv).rgb; #elif defined(_UVMODE_WORLDXZ) // 世界空间 UV:从正上方往下“投影”,平地完美,陡坡被拉成竖条 albedo = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, i.positionWS.xz * _Tiling).rgb; #else float3 w = TriplanarWeights(n, _Sharpness); #if defined(_UVMODE_WEIGHTS) albedo = w; // 调试:R=X 投影 G=Y 投影 B=Z 投影 #else TriplanarUV uv = GetTriplanarUV(i.positionWS, n, _Tiling); albedo = SampleTriplanar(TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap), uv, w).rgb; #endif #endif return Ch20ShadePBR(i.positionWS, n, i.positionCS, albedo, 0, 0.15, 1, i.fogFactor); }
vec4 frag() { vec3 n = normalize(normalWS); vec3 albedo; #if defined(_UVMODE_MESHUV) albedo = texture(_BaseMap, uv).rgb; #elif defined(_UVMODE_WORLDXZ) // 世界空间 UV:从正上方往下“投影”,平地完美,陡坡被拉成竖条 albedo = texture(_BaseMap, positionWS.xz * _Tiling).rgb; #else vec3 w = TriplanarWeights(n, _Sharpness); #if defined(_UVMODE_WEIGHTS) albedo = w; // 调试:R=X 投影 G=Y 投影 B=Z 投影 #else TriplanarUV tuv = GetTriplanarUV(positionWS, n, _Tiling); albedo = SampleTriplanar(_BaseMap, tuv, w).rgb; #endif #endif return Ch20ShadePBR(positionWS, n, gl_FragCoord, albedo, 0.0, 0.15, 1.0, fogFactor); }

左图和中图几乎一样,这正说明高度场网格的 UV 本质上就是一次 XZ 投影。注意中图格子的大小与左图不同:世界 UV 的平铺是按米算的(_Tiling = 0.5,即每 2 米一格),与网格尺寸无关。
坑:KeywordEnum 与代码切换用
[KeywordEnum(MeshUV, WorldXZ, Triplanar, Weights)] 声明的属性,材质创建时会按默认值自动打开第一个关键字 _UVMODE_MESHUV。如果在 C# 里只 EnableKeyword("_UVMODE_TRIPLANAR"),两个关键字同时开着,Unity 会选中 #pragma 列表里靠前的那个,看上去“切换没有生效”。正确做法是先关掉同组的全部关键字再打开一个,并同步 float 属性值:Assets/Cookbook/Ch20/Editor/Ch20Shots.cs(节选:keywordEnum)
// KeywordEnum 在材质创建时会按属性默认值打开第一个关键字(_UVMODE_MESHUV)。 // 代码里切换时要“关掉全部、再打开一个”,并同步 float 属性,否则 Inspector 与实际变体不一致。 static readonly string[] s_UVModes = { "_UVMODE_MESHUV", "_UVMODE_WORLDXZ", "_UVMODE_TRIPLANAR", "_UVMODE_WEIGHTS" }; static void SetUVMode(Material m, int mode) { foreach (var k in s_UVModes) m.DisableKeyword(k); m.EnableKeyword(s_UVModes[mode]); m.SetFloat("_UVMode", mode); }
本案例出自《URP 渲染实战》第 20 章「地形与植被」。在章节中阅读上下文 →