I have been asked a few times how I realised the sandy blend shader I have created for my game and want to show you how it works. First off here is a picture:
The idea behind the shader is very simple. The preconditions are that the texture(s) that can be blended need to be world-mapped. World mapping simply uses the world coordinates of a vertex as the uvs to sample a texture. In case of a terrain you might use X and Z coordinates.
void vert (inout appdata_full v, out Input o) {
fixed3 pos = mul(_Object2World, v.vertex).xyz;
o.wUV = float2(pos.x * 0.1 ,pos.z * 0.1);
}
Now we can simply use the input in the fragment or surface shader to sample the textures. I also use a float value to alter the tiling for different textures. The good thing is that we safe quite some registers for all the textures this way. (At least I think so >.<)
void surf (Input IN, inout SurfaceOutput o) {
fixed4 tBase = tex2D(_BaseTex, IN.wUV *_BaseTile);
...
}
Basically this is all about the terrain shader. Now we have to add this to our object shader.
The Object belnd shader blends the terrain texture(s) with those of the object according to the Y-coordinate of the vertex in world space. The vertex shader looks pretty similar. We only need the Y value as well.
void vert (inout appdata_full v, out Input o) {
fixed3 pos = mul(_Object2World, v.vertex).xyz;
o.wUV = float3(pos.x * 0.1 ,pos.z * 0.1, pos.y);
}
To blend the texture we need to know it and its settings. To make it easy I simply use Shader.SetGlobal* methods such as those:
Shader.SetGlobalTexture("_MapSandBaseTex", TerrainDataMat.GetTexture("_BaseTex"));
Shader.SetGlobalFloat("_MapSandBaseTile", TerrainDataMat.GetFloat("_BaseTile"));
Now you can access those values from all shaders without defining properties and use them as follows.
sampler2D _MapSandBaseTex;
half _MapSandBaseTile;
//In surface or fragment Shader:
fixed4 texD = tex2D(_MapSandBaseTex, IN.wUV.xy * _MapSandBaseTile);
To blend the texture with the object we need to define a mask. The easiest way to do so is to blend the Y-value linearly to a fixed parameter which I called _DirtHeight. The value can also be negative. Keep in mind that IN.wUV.z is the world-y coordinate.
Also currently this only work s for objects with the root on Y=0. If we want to have objects with a different height then we would have to pass an offset value as well which we would simply subtract from the world-y position.
fixed mask = saturate((_DirtHeight - IN.wUV.z) * (1 / max(0.01,_DirtHeight)));
The result we get is a value from 0 to 1 which 1 on the bottom of the object.
We are ready to blend the texture with the base texture. Also we can do more things if we want like blending specular values and others. But you maybe have to keep the various settings of the terrain in mind which I do not use here.
o.Albedo = lerp(dif, texD.rgb, mask);
o.Gloss = _Spec * lerp(tex.a, texD.a, mask);
Now the result is the picture on top in the picture below. But we want the result on the bottom which provides a much smoother look.
What I do is I blend the normals of the object with those of the terrain, which are assumed to be Y-Up or Vector3.Up = (0,1,0). Also I only blend the vertex normals, details from a normal map are still visible under the sand. That is really great!
Here is the full vertex shader code. As you can see the mask is created as in the fragment shader but we use another value called _DirtHeightNorm because you often want this value to be around half as big as the actual texture blend parameter.
Normals of an object are in object space so we have to convert our terrain normal to object space before we apply it to the mesh.
void vert (inout appdata_full v, out Input o) {
fixed3 pos = mul(_Object2World, v.vertex).xyz;
o.wUV = float3(pos.x * 0.1 ,pos.z * 0.1, pos.y);
fixed mask = saturate((_DirtHeightNorm - pos.y) * (1 / max(0.01,_DirtHeightNorm)));
v.normal = lerp(v.normal, mul(_World2Object, float4(0,1,0,0)).xyz, mask);
}
Basically that’s it. I only did some more work because our terrain system does not just have sand and also has effects like wetness which darkens the sand and makes it more glossy. The color change also has to happen in this blend shader otherwise it looks strange.
I hope this is helpful and understandable although this is not a step by step tutorial for newbies.
If you have some ideas to improve this shader let me know although it is not optimized yet.
Maybe I manage to upload a simple version of this shader in the next hours or days as well so you can test it right away.
Thanks for following our project!

