TerrainStitcher v1.0 - free for non-commercial use

One of the most asked questions concerning terrain in Unity is how to stitch together multiple terrains in order to create a large and seamless world. I was surprised (and a bit annoyed) that such an elementary feature isn’t present in the Unity editor or API, so I decided to implement it myself and share it with the community. I intend to use it myself to stitch together lots of procedurally generated terrains to create one massive world.

I present to you TerrainStitcher, it is a Unity editor and API tool that stitches terrains together by adjusting their heightmaps along the borders that “connect” the terrains. This creates a seamless transition from one terrain to the other and makes manually adjusting terrain borders unnecessary. The most important feature is automatic stitching, which means you simply layout the terrains in the editor in the way you would like them to be stitched together (exact positioning isn’t necessary), select the terrains, click ‘Stitch Selected’ and the tool will figure out the rest!

Automatic stitching

Terrains must have the same dimensions (x, y, z) and resolution for stitching to work! Undo is supported.

It is free for private, non-commercial use (donations are appreciated, if you found it particularly useful/timesaving)! For business or commercial use, a license must be obtained by paying a license fee of €10 per user (you are welcome to try it out for free as long as you buy a license when you’re actually using it for a commercial product).

Payments can be made through paypal (reijerh AT gmail.com) or bank transfer (please contact me at reijerh AT gmail.com), or by simply following this link: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=SJJW6K9UPF4ZU&lc=NL&item_name=Terrain%20Stitcher&currency_code=EUR&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted. Licenses remain valid for all future updates and bugfixes. Please mention your (company) name when making the payment.

IMO it’d be much less of a hassle for Unity users if this feature was included in the engine (you are welcome to use my code in exchange for a pro license :sunglasses:).

662286–23750–$TerrainStitcher_1_1.unitypackage (7.77 KB)

Is there a way to cut out areas of unused terrain?

I’m not sure what you mean, could you explain what you mean by ‘unused terrain’?

I have a very big terrain, but I am only using like 1/5th of it. There is lots of unused space. It would be great to have a tool that allows you to cut out the area’s that are not relavent. Possibly there is a way. But it is unknown to me.

Also have you found a clever and easy way to bring in GIS or real topo data easily, without some crazy workflow.

Thanks.

I’m sorry, this tool is purely for stitching terrains :). I haven’t worked with real topo data myself so I can’t really comment on that, you should probably convert it to heightmap format and import that heightmap into Unity. I’d be surprised if there are no people with experience in this field on these forums though (or answers.unity3d.com), so maybe try creating a topic/question :).

Sounds like you’re looking for a terrain paging or culling system. There have been a couple of scripts posted in the forums and there’s also this which is still being developed: http://forum.unity3d.com/threads/96190-Terrain-Manager

Thank you very much for your help.

It seem interesting! I would try it with real data when i come back from vacation.
Now I installed the package and I think that the Auto stitching feature could be a great feature.
For my terrain navigation project I used the nice Eric Stitchscape but i think that is a bit hard to set (each one manually) with many terrains (in my case 45 topographic tiles).
Regards
Max

I’ve drastically improved the speed when stitching multiple terrains together, this will probably be very useful for you Sjm Tech! Stitching multiple terrains now takes about the same time as stitching one terrain (in the same order anyway).

Also changed the menu item text from “Terrain Stitcher” to “Terrain Stitcher…”.

(Added version 1.1 to the first post in this thread)

@reijerh - The link no longer works.

If you mean the download link: I was probably still busy uploading the attachment or something, the file is hosted here as an attachment and it seems to work for me…

edit: would it be possible for a mod to remove the v1.0 from the thread title? Thought I could edit the title when I created the thread.

Ok, the link now works here as well…

Great improvement!
I think that you could add (for create more interest):

  • more detail regards the feature of your tool in the main description (like autostitching feature).
  • a screenshoot of the interface.
  • a video that shows a pratical example of utilization.

As i said …i reserve my working impressions when i’ll make a real test on it.
For now compliments
Max

Just sent you $26 donation for commercial use of your script.
Thank you!.

1 Like

Thanks! “Crafting focused sandbox mmo”, sounds good :).

Hi Reijer Eric,

It’s there a way to stitch terrains with different heights? I tried both Stitchscape and TerrainStitcher with no success, both scripts looks having as requirement the Y (elevation) value to be the same. It not work for heightmaps of real-world terrains.

Thanks for any idea.

Nastasache, do you mean how it sets the Y position of the terrain’s transforms to be the same? I noticed that too, although it’s not an issue for me. I’m sure the script could be modified to take the terrain’s existing Y position into account rather than adjusting it.

Reijerh, I just sent you €10 for a commercial license. Thanks for writing this so I didn’t have to.

Thanks Nition :slight_smile:

nastasache: I honestly can’t remember how it works anymore, it’s been a long time since I wrote the script and haven’t really used Unity since, sorry.

Very old post revoking, but is there any chance any one have ported this to C#? I’ve tried my self but it seems to make very small gaps there it is not stitched, my personal guess is that this is due to rounding errors. There is no real particular reason for the port other than I prefer C# over JS and find C# easier to read and work with. You may take the code and put into your package if you can make it work.

Image over the problem (Happens to all terrain from what I’ve tried so far): https://dl.dropboxusercontent.com/u/13581369/UnityTerrain.png

I included the source I’ve made so far here:

using System;
using UnityEngine;
using System.Collections;

public static class TerrainTools
{
    static double[] smoothModTable = null;
    static double[] smoothDYTable = null;
    static bool memoizationTablesFilled = false;

    static Vector3 prevSize = new Vector3(0, 0, 0);
    static int prevHeight = 0;
    static int prevWidth = 0;
    static int prevNumSamples = 0;

    delegate float GetYMod(int domTerrain, int terrainToMod, double dY, int curCellX, int maxCellsX, int curCellY, int maxCellsY);

    public static void StitchTerrains(Terrain terrain1, Terrain terrain2, int numSamples, int domTerrain)
    {
        var dX = terrain2.transform.position.x - terrain1.transform.position.x;
        var dZ = terrain2.transform.position.z - terrain1.transform.position.z;

        var height = terrain1.terrainData.heightmapHeight;
        var width = terrain1.terrainData.heightmapWidth;

        if (height != terrain2.terrainData.heightmapHeight || width != terrain2.terrainData.heightmapWidth || terrain1.terrainData.size != terrain2.terrainData.size)
        {
            return;
        }

        if (height != prevHeight || width != prevWidth || terrain1.terrainData.size != prevSize || numSamples != prevNumSamples)
        {
            prevHeight = height;
            prevWidth = width;
            prevSize = terrain1.terrainData.size;
            prevNumSamples = numSamples;

            memoizationTablesFilled = false;
            smoothModTable = new double[height];
            smoothDYTable = new double[numSamples];
        }
      
        GetYMod getYMod;
        if (memoizationTablesFilled)
        {
            getYMod = getYModMemoized;
        }
        else
        {
            getYMod = getYModDynamic;
        }

        var heights1 = terrain1.terrainData.GetHeights(0, 0, width, height);
        var heights2 = terrain2.terrainData.GetHeights(0, 0, width, height);

        if (Mathf.Abs(dX) > Mathf.Abs(dZ))
        {
            var xDir = terrain2.transform.position.x > terrain1.transform.position.x ? 1 : -1;

            terrain2.transform.position = terrain1.transform.position;
            terrain2.transform.position = new Vector3(terrain1.transform.position.x + terrain1.terrainData.size.x * xDir, terrain2.transform.position.y, terrain2.transform.position.z);

            for (int z = 0; z < height; z++)
            {
                var dY = 0.0;
                if (xDir == 1)
                {
                    dY = heights2[z, 0] - heights1[z, width - 1];
                }
                else
                {
                    dY = heights2[z, width - 1] - heights1[z, 0];
                }

                for (int i = 0; i < numSamples; i++)
                {
                    if (xDir == 1)
                    {
                        heights1[z, width - 1 - i] = heights1[z, width - 1 - i] + getYMod.Invoke(domTerrain, 1, dY, z, height, i, numSamples);
                        heights2[z, i] = heights2[z, i] - getYMod.Invoke(domTerrain, 2, dY, z, height, i, numSamples);
                    }
                    else
                    {
                        heights1[z, i] = heights1[z, i] + getYMod.Invoke(domTerrain, 1, dY, z, height, i, numSamples);
                        heights2[z, width - 1 - i] = heights2[z, width - 1 - i] - getYMod.Invoke(domTerrain, 2, dY, z, height, i, numSamples);
                    }
                }
            }
        }
        else
        {
            var zDir = terrain2.transform.position.z > terrain1.transform.position.z ? 1 : -1;

            terrain2.transform.position = terrain1.transform.position;
            terrain2.transform.position = new Vector3(terrain2.transform.position.x, terrain2.transform.position.y, terrain1.transform.position.z + terrain1.terrainData.size.z * zDir);

            for (int x = 0; x < height; x++)
            {
                var dY = 0.0;
                if (zDir == 1)
                {
                    dY = heights2[0, x] - heights1[width - 1, x];
                }
                else
                {
                    dY = heights2[width - 1, x] - heights1[0, x];
                }
                for (int i = 0; i < numSamples; i++)
                {
                    if (zDir == 1)
                    {
                        heights1[width - 1 - i, x] = heights1[width - 1 - i, x] + getYMod.Invoke(domTerrain, 1, dY, x, height, i, numSamples);
                        heights2[i, x] = heights2[i, x] - getYMod.Invoke(domTerrain, 2, dY, x, height, i, numSamples);
                    }
                    else
                    {
                        heights1[i, x] = heights1[i, x] + getYMod.Invoke(domTerrain, 1, dY, x, height, i, numSamples);
                        heights2[width - 1 - i, x] = heights2[width - 1 - i, x] - getYMod.Invoke(domTerrain, 2, dY, x, height, i, numSamples);
                    }
                }
            }
        }

        memoizationTablesFilled = true;

        terrain1.terrainData.SetHeights(0, 0, heights1);
        terrain1.Flush();
        terrain2.terrainData.SetHeights(0, 0, heights2);
        terrain2.Flush();
    }

    static float getYModDynamic(int domTerrain, int terrainToMod, double dY, int curCellX, int maxCellsX, int curCellY, int maxCellsY)
    {
        double yMod = dY / 2.0f;

        if (terrainToMod == 1)
        {
            if (domTerrain == 1)
            {
                yMod = dY * smoothMod(curCellX, maxCellsX);
            }
            else if (domTerrain == 2)
            {
                yMod = dY * (1.0f - smoothMod(curCellX, maxCellsX));
            }
        }
        else if (terrainToMod == 2)
        {
            if (domTerrain == 1)
            {
                yMod = dY * (1.0f - smoothMod(curCellX, maxCellsX));
            }
            else if (domTerrain == 2)
            {
                yMod = dY * smoothMod(curCellX, maxCellsX);
            }
        }
        else
        {
            Debug.LogError("terrainToMod must be either 1 or 2! (found: " + terrainToMod + ")");
        }

        return (float)(yMod * smoothDY(curCellY, maxCellsY));
    }

    static float getYModMemoized(int domTerrain, int terrainToMod, double dY, int curCellX, int maxCellsX, int curCellY, int maxCellsY)
    {
        double yMod = (double)dY / 2.0f;

        if (terrainToMod == 1)
        {
            if (domTerrain == 1)
            {
                yMod = dY * smoothModTable[curCellX];
            }
            else if (domTerrain == 2)
            {
                yMod = dY * (1.0f - smoothModTable[curCellX]);
            }
        }
        else if (terrainToMod == 2)
        {
            if (domTerrain == 1)
            {
                yMod = dY * (1.0f - smoothModTable[curCellX]);
            }
            else if (domTerrain == 2)
            {
                yMod = dY * smoothModTable[curCellX];
            }
        }
        else
        {
            Debug.LogError("terrainToMod must be either 1 or 2! (found: " + terrainToMod + ")");
        }

        return (float)(yMod * smoothDYTable[curCellY]);
    }

    static double smoothDY(int current, double max)
    {
        double x = 1.0f - (double)current / max - 1;

        var result = (236706659320000.0 * Math.Pow(x, 6) + 99115929736349168000.0 * Math.Pow(x, 5) - 247790818523389713100.0 * Math.Pow(x, 4)
            + 80036273392876468420.0 * Math.Pow(x, 3) + 127736693875550215551.0 * Math.Pow(x, 2) - 713647159857677493.0 * x) / 58384668816317760000.0;
        smoothDYTable[current] = result;
        return result;
    }

    static double smoothMod(int current, double max)
    {
        double x = (double)current / max - 1;

        var result = 1.0E-4 * ((1 * (x - 0.0) * (x - 0.05) * (x - 0.1) * (x - 0.5) * (x - 0.9) * (x - 0.95) * (x - 0.99) * (x - 1.0)) / -1.4317846804800003E-5) +
            0.0050 * ((1 * (x - 0.0) * (x - 0.01) * (x - 0.1) * (x - 0.5) * (x - 0.9) * (x - 0.95) * (x - 0.99) * (x - 1.0)) / 3.074152499999999E-5) +
            0.04 * ((1 * (x - 0.0) * (x - 0.01) * (x - 0.05) * (x - 0.5) * (x - 0.9) * (x - 0.95) * (x - 0.99) * (x - 1.0)) / -9.804240000000002E-5) +
            0.5 * ((1 * (x - 0.0) * (x - 0.01) * (x - 0.05) * (x - 0.1) * (x - 0.9) * (x - 0.95) * (x - 0.99) * (x - 1.0)) / 0.0019448099999999997) +
            0.04 * ((1 * (x - 0.0) * (x - 0.01) * (x - 0.05) * (x - 0.1) * (x - 0.5) * (x - 0.95) * (x - 0.99) * (x - 1.0)) / -9.804239999999985E-5) +
            0.0050 * ((1 * (x - 0.0) * (x - 0.01) * (x - 0.05) * (x - 0.1) * (x - 0.5) * (x - 0.9) * (x - 0.99) * (x - 1.0)) / 3.0741525000000005E-5) +
            1.0E-4 * ((1 * (x - 0.0) * (x - 0.01) * (x - 0.05) * (x - 0.1) * (x - 0.5) * (x - 0.9) * (x - 0.95) * (x - 1.0)) / -1.4317846804800018E-5);
        smoothModTable[current] = result;
        return result;
    }
}

Ok this is probably a dead thread and i know its a stupid question, but how do i get it to work? what do I do to install and how do I get it to actually open up