Skip to content

TriplanarMapping

在基于高度图生成的地形中都会出现立面的纹理拉伸问题,Unity自带的地形系统同样会出现该问题。导致这个问题的原因也很简单:立面中所使用的UV只能跨越较小的纹理尺寸,进而导致了纹理的拉伸问题。

GPU Gems 3 在章节Generating Complex Procedural Terrains Using the GPU中提到使用Triplanar Mapping技术来处理立面中纹理拉伸问题。先来理解一下解决思路。

如上图,我们用ab,bc代表悬崖的两个侧面。Pa,Pb,Pc表示三个点在Y轴上的投影位置,Pa’,Pb’,Pc’,表示三个点在X轴上的投影位置。可以看到ab在Y轴上的投影距离大于X轴上的投影距离,bc则是X轴上的投影距离大于Y轴上的投影距离。所以为了让纹理坐标跨越较大范围,ab的UV应该使用其在Y轴上的投影,bc的UV应该使用其在X轴上的投影。

接下来验证一下,利用高度图拉起的地形中,UV坐标一般是使用顶点的世界坐标的xz,上图中Unity Terrain就是使用顶点坐标的xz来做UV的效果。我们首先将UV换成zy平面坐标来看下效果:

可以明显看到部分立面的拉伸已经得到很大的改善。再将UV换成xy平面坐标:

可以看到,由于观察角度不同就需要使用不同平面坐标来进行采样。比如沿x轴观察使用yz平面更容易获得好的采样结果。Triplanar Mapping技术会对三个平面分别进行采样,然后根据法线(WorldSpace)计算出权重来做最终混合,从而得到比较不错的效果。

half4 frag(v2f i) : COLOR 
{  
	float2 uv_x = TRANSFORM_TEX(i.worldPos.zy, _Basemap); // x 平面
	float2 uv_y = TRANSFORM_TEX(i.worldPos.xz, _Basemap); // y 平面
	float2 uv_z = TRANSFORM_TEX(i.worldPos.xy, _Basemap); // z 平面

	half4 col_x = tex2D(_Basemap, uv_x);
	half4 col_y = tex2D(_Basemap, uv_y);
	half4 col_z = tex2D(_Basemap, uv_z);

	half3 blend_weights = abs(i.worldNormal);
	 // 法线的分量相加会超过3,我们需要控制三个分量相加等于 1
	blend_weights  = blend_weights  /  (blend_weights.x + blend_weights.y + blend_weights.z);

	col_x *= blend_weights.x;
	col_y *= blend_weights.y;
	col_z *= blend_weights.z;

	Light mainLight = GetMainLight();
	half3 lightDir = mainLight.direction;
	half3 NdotL = max(0, dot(i.worldNormal, lightDir));
	half4 light = half4(NdotL * mainLight.color.xyz + SampleSH(i.worldNormal).xyz, 1);

	half4 diffuse = col_x + col_y + col_z;
	half4 color = diffuse * light;

	return color;
}

GPU Gems 3的Triplanar Mapping实现效果基本上就是上图的样子,但感觉混合效果并不是很好。三次采样直接混合在一起导致画面稍微有些杂乱,这里可以使用pow处理混合权重,这样原本低的权重值会更低,而又不会改变最高值,进而减少权重低的混合。

half3 blend_weights = abs(i.worldNormal);
blend_weights = pow(weights, 64);
blend_weights  = blend_weights  / (blend_weights.x + blend_weights.y + blend_weights.z);

https://developer.nvidia.com/gpugems/GPUGems3/gpugems3_ch01.html

https://www.ronja-tutorials.com/2018/05/11/triplanar-mapping.html