Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.

Adding detail to Procedurally Generated Terrain

Discussion in 'Scripting' started by BlueSin, Apr 15, 2014.

  1. BlueSin


    Apr 26, 2013
    So my team and I are creating a 2D game that has procedurally generated terrain similar to Terraria and Starbound. We used this tutorial to create our map: Unity Voxel Terrain. It look alot of different methods and effort, but we have the map generation part working just fine.

    The problem is when it comes to detail, detail of the caves and terrain contour for example. Take these 2 screenshots of our terrain, the first is a map overview, and the second is more zoomed in:

    $2D Map 2.PNG
    $2D Map.PNG

    As you can see we have all different shapes of stone patches (light grey), dirt patches, caves (dark grey). You can even see we have the map divided into layers composed of different materials. Now with that said, lets take a snippet of a Terraria map as another example:


    The difference in detail is night and day between the two, but I could just be being picky. Some things we couldn't even imagine how to do, for example the floating islands. But my biggest concern in particular is the terrain contour and cave detail. In the Terraria example it seems as though they have some perlin worms and possibly overlapping caves. But we can't seem to find solid data on how to create perlin worms. Also note the greenish dungeon looking area, and even the shapes of some of the biomes such as desert which has diagonal edges.

    All of these are puzzles to us, that we are having some issues resolving. We are using the Unity PerlinNoise method to generate our terrain. Does anybody have any tips, tricks, etc. to get our terrain generation looking more like Terraria?
  2. Vanamerax


    Jan 12, 2012
    The builtin Mathf.PerlinNoise is a bit limited to the things you can create with it. There are lots of different types of noise that you can combine in many interesting ways. I'd suggest you look up the C# port of libnoise. It has a lot of different types of noise implemented and also gives you some hints how to combine them.
  3. bigmisterb


    Nov 6, 2010
    Perlin worms:

    First, devise a brush. Something like a simple method that erases, or adds voxels based on a point and radius. You could even design it based on a texture, where the texture is either black or white, (black would remove, white would ignore) You could also add in a feature that changes voxels on the edge of the brush to something, or based on something. A perlin worm is a worm with a general direction. That direction steps 1 or more based on your settings and the direction changes based on Perlin noise values. So, X,Y would add Perlin of RandomX, RandomY. (the default perlin gives you a number that is zero to 1 I think. So you would subtract 1 and multiply it by some factor for variation.
  4. rrh


    Jul 12, 2012
    I was just watching this the other day, it's the creators of Sir, You Are Being Hunted and they mention some stuff about procedurally generated landscapes that might inspire you:

    Comparing your exmple to the Terraria map, I think you should try to vary the depth of the layers slightly more. Also use different scale of noise for each layer. But yeah, it looks like a lot of the caverns are carved out by something moving along and erasing as it goes, rather than just perlin noise.

    Probably do a broad-level approach, like divide the map into large sections, (let's say triangles) then have the triangles assigned random terrain types. Perhaps some terrains have restrictions, like it must be a triangle within this distance of the surface, or it must have five adjacent triangles. Then for all these triangles you run a different algorithm based on terrain type. Maybe some of the terrain types are basically the same, but then they add a special cavern in the middle of the region and then create a tunnel to the surface.
  5. bigmisterb


    Nov 6, 2010
    I am replying to your PM here:

    OK, first, a worm is nothing but p1 to p2 where you draw (or undraw) each 1 unit given a radius. This makes a worm (or bores a tunnel) radius wide X distance.

    Once you get this, next add radius over distance, so say we go from p1 to p2 where the radius changes from r1 to r2 over that distance. In general, I would just go from r1 to zero. This just draws the tunnel and tapers it down.

    Hint, you can calculate the distance remaining by first capturing the total distance between p1 and p2 and comparing it to the current distance between p1 and p2. (stating that you advance p1 towards p2. i.e. p1 = Vector2.MoveTowards(p1, p2, 1); ) see code below.

    Next, add some noise to this. Using PerlinNoise, you can add variety to either the radius, or the position. Radius is simple, just add the noise by the radius. (radius += noise * (radius * 0.5f); ) Position is fairly easy. To do so, you will need to know the Cross of the vector. (see below) Now it is simply a matter of adding the cross to the position by an amount -1 to 1. (noise * 2 -1 or just noise if you are using noiseLib).

    Note, when you start your noise addition to position, always temper the start point. So your noise should gradually raise from 0 to full strength over the first quarter of your tunnel. Not doing this simply starts the new tunnel offset the noise amount from the origin, which is what we don't want.

    OK, so now, if you go from 0 to 100 units away, and fade your radius from 10 to 0, add noise for vertical movement and minor noise for radius movement, you will get a perlin worm.

    Yet more advanced:
    Now, lets look at branches. Decide up front how many branches you want, then as you draw the worm, you get to point 0.333 (for random sake) and you add another worm that shoots either up or down. (again, use the Cross, or aCross from below). you can then add some trickery, such as the RandomDirection() below to make it where they are not all just 90 degree angles. Then just multiply that by the distance you want to go (could be random or based off the original distance, or distance remaining in the tunnel.) and call the same function that made the worm to start. You could also branch this one too. however, I would add a variable in on the function that will limit the number of branches. (say, the number of branches from this one -1) This way you don't get into a never ending loop while creating them. Eventually, they just wont spawn more branches. Lastly, radius should go from the current (or maybe less than the current) to zero over that distance.

    Code (csharp):
    2. // use this to get your original distance
    3. // float sqrDist = (p2 - p1).sqrMagnitude;
    6. // current percentage of motion
    7. float CurrentPercent(Vector2 p1, Vector2 p2, float sqrDist){
    8.     return (p2-p1.sqrMagnitude) / sqrDist;
    9. }
    11. // rotate the direction counter clockwise 90 degrees
    12. Vector2 Cross(Vector2 p1, Vector2 p2){
    13.     Vector2 dir = (p2-p1).normalized;
    14.     return new Vector2(-dir.y, dir.x);
    15. }
    17. // rotate the direction clockwise 90 degrees
    18. Vector2 aCross(Vector2 p1, Vector2 p2){
    19.     Vector2 dir = (p2-p1).normalized;
    20.     return new Vector2(dir.y, -dir.x);
    21. }
    23. Vector2 RandomDirection(Vector2 normal){
    24.     return (normal * 2 + Random.insideUnitSphere).normalized;
    25. }
    Last edited: Apr 23, 2014
  6. bigmisterb


    Nov 6, 2010
    Oh, also, you had asked about using the position of the point as a reference to the noise. If you haven't figured it out by now. (which you should) the noise is very jagged at a 1x1 ratio, so you need to increase the points which you pull from. (or develop a scaling)

    So tunnel that begins at 100x100 gets it's noise from 1x1 so your scaling is 0.01f.

    Also, in order to mix things up, I always use an offset (usually I just use a Random.insideUnitCircle (or Sphere if you are using libNoise) and multiply that by 10000 or so. This always gives me good results because it is not always coming from the same area. (yes, x and y values for your perlin generator)
  7. BlueSin


    Apr 26, 2013
    This is going to sound ridiculous but do I create a radial brush whose size I can control on demand? I haven't ever had to do anything like that before, and when I try to Google the solution all I find is solutions involving System.Drawing and ellipses, nothing actually stating how. The only way I could think of is to create a manual brush like this:

    int[,] wormBrush = new int[5, 5]
    0, 0, 1, 0, 0
    0, 1, 1, 1, 0
    1, 1, 1, 1, 1
    0, 1, 1, 1, 0
    0, 0, 1, 0, 0

    Other than that, I found a solution last night that creates the worms, they are looping around on themselves and traversing the map happily now. But altering their size would be an awesome addition. Here is the worm code I created, tell me what you think:

    Code (csharp):
    2. private void CreateWorm(ref BlockData[,] blocks, TerrainType terrain)
    3.     {
    4.         Vector2 wormDirection = new Vector2(
    5.             Mathf.Floor(UnityEngine.Random.Range(-1f, 1f)),
    6.             Mathf.Floor(UnityEngine.Random.Range(-1f, 1f)));
    7.         int wormLifetimeMax = UnityEngine.Random.Range(50, 101);
    8.         int currentWormSteps = 0;
    9.         Vector2 currentWormPosition = new Vector2(
    10.             Mathf.Floor(UnityEngine.Random.Range(0f, dimensions.x)),
    11.             Mathf.Floor(UnityEngine.Random.Range(0f, dimensions.y)));
    13.         while (currentWormSteps < wormLifetimeMax)
    14.         {
    15.             // The amount of steps the worm will take this turn
    16.             int moveAmount = UnityEngine.Random.Range(5, 11);
    18.             // Until we have taken the appropriate number of steps, loop
    19.             for (int currentStep = 0; currentStep < moveAmount; currentStep++)
    20.             {
    21.                 // Validate worm position and punch blocks
    22.                 if (!ValidPosition(currentWormPosition, 1, ref blocks, terrain))
    23.                     return;
    25.                 // Move the worm
    26.                 currentWormPosition += wormDirection;
    28.                 // Increase worm steps
    29.                 currentWormSteps++;
    30.             }
    32.             // Will the worm move a negative amount?
    33.             bool isNegativeX = false;
    34.             bool isNegativeY = false;
    36.             // Decide if the worm will move a negative amount
    37.             if (UnityEngine.Random.Range(0, 2) == 1)
    38.                 isNegativeX = true;
    39.             if (UnityEngine.Random.Range(0, 2) == 1)
    40.                 isNegativeY = true;
    42.             // Create a new direction based on random perlin
    43.             if (isNegativeX  isNegativeY)
    44.             {
    45.                 // Get a new random perlin direction
    46.                 wormDirection = new Vector2(
    47.                 -Mathf.RoundToInt(PerlinNoise.GetNoise(UnityEngine.Random.value, UnityEngine.Random.value, 0)),
    48.                 -Mathf.RoundToInt(PerlinNoise.GetNoise(UnityEngine.Random.value, UnityEngine.Random.value, 0)));
    49.             }
    50.             else if (isNegativeX)
    51.             {
    52.                 // Get a new random perlin direction
    53.                 wormDirection = new Vector2(
    54.                 -Mathf.RoundToInt(PerlinNoise.GetNoise(UnityEngine.Random.value, UnityEngine.Random.value, 0)),
    55.                 Mathf.RoundToInt(PerlinNoise.GetNoise(UnityEngine.Random.value, UnityEngine.Random.value, 0)));
    56.             }
    57.             else if (isNegativeY)
    58.             {
    59.                 // Get a new random perlin direction
    60.                 wormDirection = new Vector2(
    61.                 Mathf.RoundToInt(PerlinNoise.GetNoise(UnityEngine.Random.value, UnityEngine.Random.value, 0)),
    62.                 -Mathf.RoundToInt(PerlinNoise.GetNoise(UnityEngine.Random.value, UnityEngine.Random.value, 0)));
    63.             }
    64.             else
    65.             {
    66.                 // Get a new random perlin direction
    67.                 wormDirection = new Vector2(
    68.                 Mathf.RoundToInt(PerlinNoise.GetNoise(UnityEngine.Random.value, UnityEngine.Random.value, 0)),
    69.                 Mathf.RoundToInt(PerlinNoise.GetNoise(UnityEngine.Random.value, UnityEngine.Random.value, 0)));
    70.             }
    71.         }
    72.     }
    The terrain is looking better already, especially when you zoom in and traverse the tunnels as a player it feels much better now. We still have adjustments to make though, the worms are moving a little too diagonal for my liking, so once I figure out this radial brush thing I will try your theory (although it feels a bit over my head lol). Check it out, using the worms to create air, dirt, and stone, in addition to perlin generated stone and dirt patches.

    Last edited: Apr 23, 2014
  8. bigmisterb


    Nov 6, 2010
    I am looking for 1 method... wherever it may be... it is the single method that sets the block to a type, or disables it.

    Other than that, a brush is easy to create. Very simple statement:
    Code (csharp):
    2. float radius = 5;
    3. float dist = radius * radius;
    4. Vector2 position = Vector2(0,0);
    5. for(float y = -radius; y<radius; y++){
    6.     for(float x = -radius; x<radius; x++){
    7.         if(new Vector2(x,y).sqrMagnitude < dist)
    8.             SetTerrain(position.x + x, position.y + y, terrainType);
    9.     }
    10. }
  9. BlueSin


    Apr 26, 2013
    Ah, yes sorry that method is very messy since I barely managed to cobble it together. Very long as well as it is inefficient. Should help now that I have the brush code now.

    Here it is, updated using your radius code:

    Code (csharp):
    2. private bool ValidPosition(Vector2 position, ref BlockData[,] blocks, TerrainType terrain, float radius)
    3.     {
    4.         // float radius = 5;
    5.         float dist = radius * radius;
    6.         // Vector2 position = new Vector2(50, 50);
    7.         for (float y = -radius; y < radius; y++)
    8.         {
    9.             for (float x = -radius; x < radius; x++)
    10.             {
    11.                 if (new Vector2(x, y).sqrMagnitude < dist)
    12.                 {
    13.                     if (position.x + x >= dimensions.x - 1 || position.x + x <= 0)
    14.                         return false;
    16.                     if (position.y + y >= dimensions.y - 1 || position.y + y <= 0)
    17.                         return false;
    19.                     blocks[(int)position.x + (int)x, (int)position.y + (int)y].Terrain = terrain;
    20.                 }
    21.             }
    22.         }
    24.         return true;
    25. }
    Edit: So I implemented the radius, it works beautifully. I am sorry to say your explanation of how to create a worm though went right over my head. Looking at the code I have now though, what can I do to get worms like these:


    Also here is what I have now:

    Last edited: Apr 24, 2014
  10. BlueSin


    Apr 26, 2013
    Sorry for the double post here but I managed to solve the problem. I believe that I was just burned out yesterday and couldn't think anymore. I had this up and running within an hour of coming in today. Check out the worms now:


    Thanks everybody for your help and suggestions, they were all well received and appreciated. The newly recoded combination of the new worms, new caves, new patches, and new contour make this look almost spot on Terraria. Thanks all!
  11. DrSnake


    Oct 17, 2014
    So why don't you post your result / the working code?
  12. Moshu


    Mar 13, 2015
    I would be very thankful for the working code or an example of how you got this working in the end. : )
  13. 321Alex


    Jul 22, 2015
    Has anyone ever done something like this in a 3D space?
  14. TheFrontline


    Mar 5, 2016
    No but I can only imagine it would just take another axis because if you notice these only mention 2 of the 3.