Search Unity

Ground texture blend with Object

Discussion in 'Shaders' started by McDev02, Jul 26, 2014.

  1. McDev02

    McDev02

    Joined:
    Nov 22, 2010
    Posts:
    664
    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.

    Code (CSharp):
    1. void vert (inout appdata_full v, out Input o) {
    2.             fixed3 pos = mul(_Object2World, v.vertex).xyz;
    3.             o.wUV = float2(pos.x * 0.1 ,pos.z * 0.1);
    4.         }
    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 >.<)

    Code (CSharp):
    1. void surf (Input IN, inout SurfaceOutput o) {
    2. fixed4 tBase = tex2D(_BaseTex,  IN.wUV  *_BaseTile);
    3. ...
    4. }
    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.
    Code (CSharp):
    1. void vert (inout appdata_full v, out Input o) {
    2.            fixed3 pos = mul(_Object2World, v.vertex).xyz;
    3.             o.wUV = float3(pos.x * 0.1 ,pos.z * 0.1, pos.y);
    4. }
    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:

    Code (CSharp):
    1. Shader.SetGlobalTexture("_MapSandBaseTex", TerrainDataMat.GetTexture("_BaseTex"));
    2.         Shader.SetGlobalFloat("_MapSandBaseTile", TerrainDataMat.GetFloat("_BaseTile"));
    Now you can access those values from all shaders without defining properties and use them as follows.
    Code (CSharp):
    1. sampler2D _MapSandBaseTex;
    2. half _MapSandBaseTile;
    3.  
    4. //In surface or fragment Shader:
    5. 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.
    Code (CSharp):
    1. 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.

    Code (CSharp):
    1. o.Albedo = lerp(dif, texD.rgb, mask);
    2. 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.

    Code (CSharp):
    1. void vert (inout appdata_full v, out Input o) {
    2.             fixed3 pos = mul(_Object2World, v.vertex).xyz;
    3.             o.wUV = float3(pos.x * 0.1 ,pos.z * 0.1, pos.y);
    4.             fixed mask = saturate((_DirtHeightNorm - pos.y) * (1 / max(0.01,_DirtHeightNorm)));
    5.  
    6.             v.normal = lerp(v.normal, mul(_World2Object, float4(0,1,0,0)).xyz, mask);
    7.         }
    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!
     
  2. FatallErroR

    FatallErroR

    Joined:
    Dec 4, 2012
    Posts:
    19
    I've been following your project for a while, since i'm developing a similar project, i need to say that this shader solution is pure class, well done.

    I wish you luck in your project mate.

    Seeya
     
  3. Brenden-Frank

    Brenden-Frank

    Joined:
    Aug 5, 2012
    Posts:
    110
    Hey this is awesome.

    It's worth mentioning if you're referencing the terrain textures, the global shader will turn up empty by referencing the material directly; you'll need to reference the texture like below:

    Code (CSharp):
    1. [RequireComponent(typeof(Terrain))]
    2. public class MeshBlend : MonoBehaviour
    3. {
    4.     void Awake()
    5.     {
    6.         Terrain terrain = GetComponent<Terrain>();
    7.         Shader.SetGlobalTexture("_TerrainSplat0", terrain.terrainData.splatPrototypes[0].texture);
    8.     }
    9. }
    It's also worth noting that this is top-down projection. If you have anything with steep walls you'll get UV stretching and need to work in a custom solution.

    I would love to know if there's a way to sample the terrain height and compensate for the offset automatically.

    Also, a way to tell the normal angle of the terrain vertex nearest the object would help since I'd be able to make the mesh blend well on hills rather than being restricted to flat surfaces.
     
    Last edited: Aug 15, 2014
    one_one likes this.
  4. one_one

    one_one

    Joined:
    May 20, 2013
    Posts:
    621
    So this is not really a solution for e.g. smoother transitions for buildings to the ground?
     
  5. Brenden-Frank

    Brenden-Frank

    Joined:
    Aug 5, 2012
    Posts:
    110
    You could maybe go with a tri-planar approach, but you are significantly increasing the complexity of your mesh shaders.
     
  6. kebrus

    kebrus

    Joined:
    Oct 10, 2011
    Posts:
    415
    I just wanna say thank you :)
     
  7. McDev02

    McDev02

    Joined:
    Nov 22, 2010
    Posts:
    664
    I am not using the Unity terrain so I didn't know that, but thanks for adding.
    You are right about the UV.stretching and I already thought about a workaround but it is not on my list now.
    The thing is that I wanted the World/Terrain UVs to make sure that there is no seam when blending the texture.
    This is something I already considered and what I want to try. There would only be issues with complex terrain shapes and big objects, but both will not be in the game.

    It basically is, but with some restrictions. I just wanted to provide the basic overview, you might adjust the shader to your needs.
     
  8. MOhi

    MOhi

    Joined:
    Jan 29, 2014
    Posts:
    21
    Thanks for the info and explanation.

    I know it's a very old post but i was wondering if by any chance you can upload a sample scene :D
    Thanks again
     
  9. Gekigengar

    Gekigengar

    Joined:
    Jan 20, 2013
    Posts:
    738
    Any way to do this with Shader Graph?
     
  10. nasos_333

    nasos_333

    Joined:
    Feb 13, 2013
    Posts:
    13,348