基于高度的纹理混合shader
原文:基于高度的纹理混合shader - 知乎 (zhihu.com)
最近支持一个使用unity2021的项目,发现urp自带的Terrain/Lit shader已经自带高度混合了,看了下和我当初写的那个基本差不多,感觉稍微要比我的严谨一些,大家感兴趣的可以参考下,代码如下:
#ifdef _TERRAIN_BLEND_HEIGHT
void HeightBasedSplatModify(inout half4 splatControl, in half4 masks[4])
{
// heights are in mask blue channel, we multiply by the splat Control weights to get combined height
half4 splatHeight = half4(masks[0].b, masks[1].b, masks[2].b, masks[3].b) * splatControl.rgba;
half maxHeight = max(splatHeight.r, max(splatHeight.g, max(splatHeight.b, splatHeight.a)));
// Ensure that the transition height is not zero.
half transition = max(_HeightTransition, 1e-5);
// This sets the highest splat to "transition", and everything else to a lower value relative to that, clamping to zero
// Then we clamp this to zero and normalize everything
half4 weightedHeights = splatHeight + transition - maxHeight.xxxx;
weightedHeights = max(0, weightedHeights);
// We need to add an epsilon here for active layers (hence the blendMask again)
// so that at least a layer shows up if everything's too low.
weightedHeights = (weightedHeights + 1e-6) * splatControl;
// Normalize (and clamp to epsilon to keep from dividing by zero)
half sumHeight = max(dot(weightedHeights, half4(1, 1, 1, 1)), 1e-6);
splatControl = weightedHeights / sumHeight.xxxx;
}
#endif
纹理混合(Texture Blend)是非常常见的着色器需求,在很多实时游戏中都需要它来实现复杂的地面纹理,参考了Advanced Terrain Texture Splatting这篇文章写了一个基于高度进行混合的shader,这里分享一下自己的理解,效果如下:
(动图请点这里:https://pic4.zhimg.com/v2-5ebb57038515b3659a7b50c7976fe7e3_b.gif)
说到贴图混合,也许你已经听说过Texture Splatting技术了,这个术语是Charles Bloom创造的,他在http://www.cbloom.com/3d/techdocs/splatting.txt里对这个技术进行了阐述;
混合的最简单做法就是,用一个通道对贴图进行插值 ,效果如下
核心代码如下
void surf (Input IN, inout SurfaceOutput o) {
fixed4 splat_control = tex2D (_Control, IN.uv_Control).rgba;
fixed3 lay1 = tex2D (_Splat0, IN.uv_Splat0);
fixed3 lay2 = tex2D (_Splat1, IN.uv_Splat1);
fixed3 lay3 = tex2D (_Splat2, IN.uv_Splat2);
fixed3 lay4 = tex2D (_Splat3, IN.uv_Splat3);
o.Alpha = 0.0;
o.Albedo.rgb = (lay1 * splat_control.r + lay2 * splat_control.g + lay3 * splat_control.b+ lay4 * splat_control.a);
}
这端代码很好理解,通过splat_control这张贴图的四个通道控制_Splat0~_Splat3这四张贴图的混合,如果splat_control对应通道的值为1,那么这个通道对应的贴图就完全显示,为0则完全不显示,通过修改splat_control贴图就可以实现想要的混合效果了;
这种技术在Unity3D的标准地形编辑器中有使用。
如你所见,过渡很平滑,但不太自然。石头看起来就好像被沙子污染了,但在现实世界中这是不可能发生的情况。沙子不会粘着石头,相反地,沙子会落下来,填补到石头之间的缝隙里,而石头表面仍是干净的。
我们希望沙子会更多的在缝隙里面出现,而石头越高的地方沙子应该要越少,那么我们需要知道每一张贴图的深度信息,这里我把贴图对应的高度图保存在每张贴图的alpha通道。通过对比每张贴图的高度差,就可以知道应该显示哪张贴图了,为了简化,我们先计算两张贴图混合的情况,代码如下:
float3 blend(float3 lay1, float3 lay2)
{
return lay1.a > lay2.a ? lay1.rgb : lay2.rgb;
}
`得到的是这么样的效果
其中用于混合的两张贴图和他们的透明通道分别是这样的:
我们加上splat_control 贴图的影响试试
float3 blend(float3 lay1, float3 lay2, float4 splat_control)
{
return lay1.a * splat_control > lay2.a *splat_control ? lay1.rgb : lay2.rgb;
}
得到这样的效果:
相比原来的线性混合,现在看起来已经自然很多了,沙子落在石砖路的缝隙里,并慢慢减少;但因为目前只是单纯的判断显示那个贴图,所以边缘看起来太硬了,人工痕迹比较明显,
为了改进效果,我们给边缘增加一点过渡。
float3 blend(float3 lay1, float3 lay2, float4 splat_control)
{
float b1 = lay1.a * splat_control.r;
float b2 = lay2.a * splat_control.g;
float ma = max(b1,b2);
b1 = max(b1 - (ma – 0.3), 0) * splat_control.r;
b2 = max(b2 - (ma – 0.3), 0) * splat_control.g;
return (lay1.rgb * b1 + lay2.rgb * b2)/(b1 + b2);
}
解释一下这段代码,先对比两张贴图的高度,高度差超过0.3的会被舍弃掉,为了防止在边缘以外的地方也被保留下来了,所以后面再乘一次splat_control,最后做一个标准化处理,把他们按比例缩放到0-1这个区间。
于是,我们就得到了下面的这个效果
看起来非常自然,沙子慢慢过渡到石砖路,砖面上的沙子比较少,缝隙里的沙子更多 。
我们把这个算法拓展到4张贴图,并通过一个值来控制混合的权重,完整代码如下:
Shader "mya/terrainTextrueBlend" {
Properties {
_Splat0 ("Layer 1(RGBA)", 2D) = "white" {}
_Splat1 ("Layer 2(RGBA)", 2D) = "white" {}
_Splat2 ("Layer 3(RGBA)", 2D) = "white" {}
_Splat3 ("Layer 4(RGBA)", 2D) = "white" {}
_Tiling3("_Tiling4 x/y", Vector)=(1,1,0,0)
_Control ("Control (RGBA)", 2D) = "white" {}
_Weight("Blend Weight" , Range(0.001,1)) = 0.2
}
SubShader {
Tags
{
"RenderType"="Opaque"
"Queue"="Geometry"
}
CGPROGRAM
#pragma surface surf BlinnPhong
#pragma target 3.0
struct Input
{
float2 uv_Control : TEXCOORD0;
float2 uv_Splat0 : TEXCOORD1;
float2 uv_Splat1 : TEXCOORD2;
float2 uv_Splat2 : TEXCOORD3;
//float2 uv_Splat3 : TEXCOORD4;
};
sampler2D _Control;
sampler2D _Splat0,_Splat1,_Splat2,_Splat3;
float _Weight;
float4 _Tiling3;
inline half4 Blend(half high1 ,half high2,half high3,half high4 , half4 control)
{
half4 blend = half4(high1, high2, high3, high4) * control;
half ma = max(blend.r, max(blend.g, max(blend.b, blend.a)));
//与权重最大的通道进行对比,高度差在_Weight范围内的将会保留,_Weight不可以为0
blend = max(blend - ma +_Weight , 0) * control;
return blend/(blend.r + blend.g + blend.b + blend.a);
}
void surf (Input IN, inout SurfaceOutput o) {
half4 splat_control = tex2D (_Control, IN.uv_Control).rgba;
half4 lay1 = tex2D (_Splat0, IN.uv_Splat0);
half4 lay2 = tex2D (_Splat1, IN.uv_Splat1);
half4 lay3 = tex2D (_Splat2, IN.uv_Splat2);
half4 lay4 = tex2D (_Splat3, IN.uv_Control*_Tiling3.xy);
//纯色测试代码
//lay1.rgb = fixed3(1,0,0);
//lay2.rgb = fixed3(0,1,0);
//lay3.rgb = fixed3(0,0,1);
//lay4.rgb = fixed3(0,0,0);
half4 blend = Blend(lay1.a,lay2.a,lay3.a,lay4.a,splat_control);
o.Alpha = 0.0;
o.Albedo.rgb = blend.r * lay1 + blend.g * lay2 + blend.b * lay3 + blend.a * lay4;//混合
}
ENDCG
}
FallBack "Diffuse"
}
最终效果:
左边混合权重为0.2,右边为1,混合权重为1的时候其实就是普通的线性混合了。
加上法线和高光的效果
最后附上文中所用的贴图:链接:http://pan.baidu.com/s/1cKodFg 密码:5kvi