Unity Terrane and Mesh Blending

Hello everyone, in June OTUS is launching the Unity Games Developer course again . In anticipation of the start of the course, we have prepared a translation of interesting material on the topic.



Today we’ll talk about how to blend a terrane mesh (or other mesh) in Unity. This guide is pretty advanced, but I tried to break it down into separate steps. It is assumed that you have general Unity skills and basic C # knowledge. The guide is designed for amplify, but I think it can also be applied to the Shader Graph. This is my first guide, so I hope that it will be clear enough. If you want to add something, let me know. I have included a starter kit here, which has some additional shaders for this project, as well as a basic kit for getting started. All project files from today's manual are available to my patrons for $ 5, but in the near future, they will be available to everyone.

Starter kit
The whole project

: , , . . , , , . ( ) unity . , , .

So, let's begin. To begin, I will single out the problem and delve into the theory on which the solution will be based. To make blending between the terrane and other meshes, you need to somehow tell the shader where the intersection with the terrane occurs. Saying is easier than doing, but there is a way. And what does it consist of? Render Texture will help us !

What is a render texture?


Render Texture is, in fact, the position of the camera, which is saved in the asset-file. Render Texture in games is most often used for things like creating video surveillance screens. We can use the Render Texture to save the camera view in the editor so that this texture can then be used as a shader. This is useful because we can bake information about our terrain, such as height, normals and colors, into a texture that can then be used in runtime to blend between meshes and terran.


Texture rendering used to display goose images on TVs in Untitled Goose Game

Customization


To start, let's create a new scene and terrane. Set some convenient size for work, for example, 200x200. Now you can arrange the terrane as you want. Then create a new layer and name it “Terrain” and assign a terrain to this layer. This is necessary so that the camera mask can record terrane in the Render Texture .


My masterpiece terrane

The project source files have a prefab called “BlendBakingCamera” - drag it onto the stage. You will get a simple orthographic camera. On camera, you need to put a culling mask on a new layer of terrane. Position the camera in the center of the terrane slightly above the highest point on the terrain. Then adjust the far clip plane so that the camera sees the terrane floor. In the end, the scene should look something like this:


Replacement shader


Now that the camera is set up, you need to find a way to record terrane data. For this we need Replacement Shaders. The Replacement Shader seems like a dubious prospect; I myself have not understood how it works for a long time. But in fact, everything is very simple and effective. Using the Replacement Shader essentially means rendering each object in the camera’s field of view with a single shader, regardless of which shader is superimposed on the objects. As a result, all objects will be rendered using the selected Replacement Shader , which is actually just a regular shader.

The shader we need for blending is the depth shader. It renders the depth of the scene and is a key component in creating our blending effect, as it writes the depth values ​​of our camera to the texture so that we can read them later. To learn more about this shader and the Replacement Shader in general, I recommend that you read this manual from Making Stuff Look Good in Unity .


Depth Shader Example

Let's start baking


Let's create a new class and name it “TerrainBlendingBaker” . Let's start by implementing a depth mask for the base terrane. Later we will return to this script to add colors and normals.

Define several variables.

//Shader that renders object based on distance to camera
public Shader depthShader;
//The render texture which will store the depth of our terrain
public RenderTexture depthTexture;
//The camera this script is attached to
private Camera cam;

Now let's create a new method and call it “UpdateBakingCamera” . In this method, we will determine the camera data that the shader may need to render blending in global variables.

private void UpdateBakingCamera()
    {
        //if the camera hasn't been assigned then assign it
        if (cam == null)
        {
            cam = GetComponent<Camera>();
        }
 
        //the total width of the bounding box of our cameras view
        Shader.SetGlobalFloat("TB_SCALE", GetComponent<Camera>().orthographicSize * 2);
        //find the bottom corner of the texture in world scale by subtracting the size of the camera from its x and z position
        Shader.SetGlobalFloat("TB_OFFSET_X", cam.transform.position.x - cam.orthographicSize);
        Shader.SetGlobalFloat("TB_OFFSET_Z", cam.transform.position.z - cam.orthographicSize);
        //we'll also need the relative y position of the camera, lets get this by subtracting the far clip plane from the camera y position
        Shader.SetGlobalFloat("TB_OFFSET_Y", cam.transform.position.y - cam.farClipPlane);
        //we'll also need the far clip plane itself to know the range of y values in the depth texture
        Shader.SetGlobalFloat("TB_FARCLIP", cam.farClipPlane);
 
        //NOTE: some of the arithmatic here could be moved to the shader but keeping it here makes the shader cleaner so ¯\_(ツ)_/¯
    }

Now let's bake the depth of the terrane into the texture.

// The context menu tag allows us to run methods from the inspector (https://docs.unity3d.com/ScriptReference/ContextMenu.html)
[ContextMenu("Bake Depth Texture")]
public void BakeTerrainDepth()
{
    //call our update camera method 
    UpdateBakingCamera();
 
    //Make sure the shader and texture are assigned in the inspector
    if (depthShader != null && depthTexture != null)
    {
        //Set the camera replacment shader to the depth shader that we will assign in the inspector 
        cam.SetReplacementShader(depthShader, "RenderType");
        //set the target render texture of the camera to the depth texture 
        cam.targetTexture = depthTexture;
        //set the render texture we just created as a global shader texture variable
        Shader.SetGlobalTexture("TB_DEPTH", depthTexture);
    }
    else
    {
        Debug.Log("You need to assign the depth shader and depth texture in the inspector");
    }
}

The values ​​will become clearer when we start working with the shader. Here is a small picture that may shed some light:


We are going to transfer the terrane to the depth texture in order to later read it and understand where to do blending.

Now we have everything we need to create the basic blending effect in the shader. At this point, the script looks something like this: pastebin.com/xNusLJfh

Okay, now that we have the script, we can add it to the baking camera that we added earlier. The initial assets have a shader called 'DepthShader' (Inresin / Shaders / DepthShader) and a Render Texture called 'DepthTerrainRT' (Inresin / RenderTextures / DepthTextureRT) , you need to put them in the appropriate fields in the inspector.

After that, simply run the method through the context menu to bake our terrain depth in the Render Texture .


Shader


Let's finally create a shader for blending. Create a new standard amplify shader and open it, name it 'TerrainBlending' or so.

Now we need to create a UV for the render texture . This will be the difference between the point that is being rendered and the baking camera position scaled relative to the total area. The three global variables here are the ones we just declared in the code. We also set worldY as a local variable, we will need it later.



Let's take the depth texture, which we assigned as a global variable (for this add texture sample node , make it global and name it 'TB_DEPTH'), if we put the output in the debug amplify shader 'a field , we can see what happens. Create a plane with the material to which our new shader will be applied.


So, in the shader, we have information about the depth, now we need to add an offset in y to get blending.



This block scales the y position of the far clip plane mask , subtracts this value from the world position along the y axis of the point that is being rendered, and then finally shifts it to the lower side of the camera’s bounding box (y position of the camera minus the farclip plane ).

Already something! We see how the edges of the plane merge with the terrane.



Ok, let's make blending more controlled.



Now we can control the thickness and area of ​​the blending terrane.



I think we can even add a little noise. Let's use the world position to generate noise from the texture.



Noise textures are in the texture folder in the startup project, they can be assigned in the inspector or as a constant in the shader itself.

Finally it's time to add some textures! To get started, let's use some simple single-color textures, I added two textures to the folder with texture assets. Make the 'SingleColorGrass' texture a terrane texture. Then in the shader you need to create a terrane texture and an object texture node . We will switch between them on the red channel of the mask that we just created.




And here is the complete shader.



Adding custom toon lighting or unlit lighting models will provide the best results for this shader. I turned on unlitterrane shader and unlit version of the shader in the full package available to sponsors.


Unlit Terrane and Meshes

I also recommend adding a triplanar shader to the terrane and possibly other meshes. I can consider this issue in the next guide.

Well, we are almost done with the main theme of today's guide. I have added some sections that can help with the shader extension.

Shader Extension - Normal


I added a regular shader to the files, you can use it to write normals to the terrane surface and also use blending. For my game, I don’t need normal blending, so I just experimented a bit and it looks like the experiment was a success, and my idea works well for terrain without a high degree of height change. I included the normal mapping shader in the directory with the starting set shaders. Here you can see my basic implementation of the normal map:



The code is almost the same as with the depth map, but this time we will use the normal map as a replacement shader . I also added a set of render texture for recording normal (here you may need additional configuration).

For the normals to work well, it may take a little more effort related to tuning, and there may also be a restriction associated with low-level terranes (but I have not conducted enough tests to confirm this).

Shader Extension - All Colors


I am not going to go deep into the details of this topic, since it goes beyond what is required for my game, but I thought of it while writing this guide. To add blending of many colors, we can select unlit terrane colors and save them as textures. In this case, we are limited by a fairly low resolution, but this method works well when using single-color terrain textures and low-resolution textures, or when using faint bleed . With minor adjustments, they can also be applied to terrane meshes.

Here is the code for the multicolor option:

[Header("The following settings are only if using the multi-color terrain shader")]
//Shader that renders the unlit terraom of an object
public Shader unlitTerrainShader;
//The render texture which will store the normals of our terrain
public RenderTexture surfaceTexture;
//An unlit terrain material used to capture the texture of our terrain without any lighting
public Material unlitTerrainMaterial;
//The terrain you want to capture the textures of
public Terrain yourTerrain;
 
[ContextMenu("Bake Surface Texture")]
public void BakeTerrainSurface()
{
    UpdateBakingCamera();
 
    //return if there is no terrain assigned
    if (yourTerrain == null)
    {
        Debug.Log("You need to assign a terrain to capture surface texture");
        return;
    }
 
    StartCoroutine(BakeColors());
}
 
IEnumerator BakeColors()
{
    Material tempTerrainMaterial = yourTerrain.materialTemplate;
 
    yourTerrain.materialTemplate = unlitTerrainMaterial;
 
    yield return 0;
 
    cam.SetReplacementShader(unlitTerrainShader, "RenderType");
    cam.targetTexture = surfaceTexture;
    Shader.SetGlobalTexture("TB_SURFACE", surfaceTexture);
 
    yield return 0;
 
    cam.targetTexture = null;
    yourTerrain.materialTemplate = tempTerrainMaterial;
 
    yield return null;
 
}

The only change in the shader is that instead of using a predefined texture, we use a globally defined terrain surface texture that will use the relative position as UV. An additional extension allows you to get a more pleasant blending of textures.


Blending multiple textures

Here is a complete multicolor graph with a normal blending shader:




Conclusion


Congratulations if you have read up to this point! As I said, this is my first guide, so I would really appreciate feedback. If you want to support me in creating games and tutorials, take a look here or subscribe to my Twitter .



Learn more about the course

All Articles