AlexStv

Unity voxel block tutorial pt. 6

So far we have a very interesting system for voxel terrain but it still doesn't look great. It needs some realistic terrain to shape the chunks. We'll be adding that using simplex noise for semi random noise. We'll use simplex noise because it can give us smooth non repeating values based on coordinates. This means that we can use the coordinates of a chunk to decide its contents and fill it the same way every time. The way Perlin noise is usually used for terrain is to take noise generated with different frequencies applied on top of each other so that you get large mountains with small rocks and roughness. I'll let people who can explain it better explain though: Perlin noise. We will be using a public domain implementation of simplex noise in C# but if you would like to take a look at implementing Perlin noise yourself take a look at CatLikeCoading's fantastic tutorial on simplex noise.

Great so we'll start from the source of noise, which we will get from this project. Create a new script called Noise and delete its automatically generated contents and replace it with the code from the simplexnoise project's noise.cs file found here: https://simplexnoise.googlecode.com/svn/trunk/SimplexNoise/Noise.cs and that should meet all our needs for noise generation. The reason we're using this and not Unity's Mathf.PerlinNoise function is that it only uses 2d coordinates to generate noise not 3d, this could be a problem if you want to use noise to create 3d caves or anything other than a height map.

Once you have your new script with content from the link above we can continue and create a new script called TerrainGen which we'll use to fill chunks with their contents. To start off put this in the new script:

using UnityEngine;
using System.Collections;
using SimplexNoise;
public class TerrainGen {
     public Chunk ChunkGen(Chunk chunk)
     {
     }
     public static int GetNoise(int x, int y, int z, float scale, int max)
     {
         return Mathf.FloorToInt( (Noise.Generate(x * scale, y * scale, z * scale) + 1f) * (max/2f));
     }
}

Here we have a class with two functions. First of all GetNoise has some content already, it serves as an easier way to call the Noise script's Noise.Generate which get's some noise for the coordinates provided. The function takes the coordinates to sample. It also takes a scale value which we will multiply the coordinates by, a smaller value here samples a smaller area giving us smooth noise suitable for mountains with long distances between the peaks and valleys but a larger value will make more frequent bumps and dips. The max parameter is the max of the return value, we will always get an int returned between zero and max.

We can start working on the ChunkGen function now, it's meant to take a chunk, fill it and return the filled chunk. It will do this by cycling over every column in the chunk and handling them individually. Add the following code to the function to make it cycle over every column with a new function to handle columns individually:

     public Chunk ChunkGen(Chunk chunk)
     {
         for (int x = chunk.pos.x; x < chunk.pos.x + Chunk.chunkSize; x++)
         {
             for (int z = chunk.pos.z; z < chunk.pos.z + Chunk.chunkSize; z++)
             {
                 chunk = ChunkColumnGen(chunk, x, z);
             }
         }
         return chunk;
     }
     public Chunk ChunkColumnGen(Chunk chunk, int x, int z)
     {
         return chunk;
     }

Now every column goes through ChunkColumnGen and the changes are added to chunk. Now we'll add some generation to the new function but first we'll need some variables to control the noise function so add these new private variables:

     float stoneBaseHeight = -24;
     float stoneBaseNoise = 0.05f;
     float stoneBaseNoiseHeight = 4;
     float stoneMountainHeight = 48;
     float stoneMountainFrequency = 0.008f;
     float stoneMinHeight = -12;
     float dirtBaseHeight = 1;
     float dirtNoise = 0.04f;
     float dirtNoiseHeight = 3;

These are variables for three separate parts of the terrain, first the base stone layer it has a base height that we'll start from, then the scale of the noise to add to the base height an the height of that noise. 0.05 for noise makes peaks around 25 blocks apart so I like it for making stone less flat. The noise height for this is 4 so the max difference between peak and valley is 4, not very much. Then we have mountain variables with a much smaller frequency value and larger height. The min height here is the lowest stone is allowed to go. And lastly we'll add a layer of dirt to the top, the base height being the minimum depth on top of the rock and the noise a little more messy than the stone with smaller peaks.

Now the meat of the generation, set up the ChunkColumnGen function like this:

     public Chunk ChunkColumnGen(Chunk chunk, int x, int z)
     {
         int stoneHeight = Mathf.FloorToInt(stoneBaseHeight);
         stoneHeight += GetNoise(x, 0, z, stoneMountainFrequency, Mathf.FloorToInt(stoneMountainHeight));
         if (stoneHeight < stoneMinHeight)
             stoneHeight = Mathf.FloorToInt(stoneMinHeight);
         stoneHeight += GetNoise(x, 0, z, stoneBaseNoise, Mathf.FloorToInt(stoneBaseNoiseHeight));
         int dirtHeight = stoneHeight + Mathf.FloorToInt(dirtBaseHeight);
         dirtHeight += GetNoise(x, 100, z, dirtNoise, Mathf.FloorToInt(dirtNoiseHeight));
         for (int y = chunk.pos.y; y < chunk.pos.y + Chunk.chunkSize; y++)
         {
             if (y <= stoneHeight)
             {
                 chunk.SetBlock(x - chunk.pos.x, y - chunk.pos.y, z - chunk.pos.z, new Block());
             }
             else if (y <= dirtHeight)
             {
                 chunk.SetBlock(x - chunk.pos.x, y - chunk.pos.y, z - chunk.pos.z, new BlockGrass());
             }
             else
             {
                 chunk.SetBlock(x - chunk.pos.x, y - chunk.pos.y, z - chunk.pos.z, new BlockAir());
             }
         }
         return chunk;
     }

So now you can see how all the variables are used. We create a stone height variable set to the base height, add the mountain noise then raise everything under the minimum to the minimum and apply the base noise. Then we make a dirt height variable equal to the stone height plus the base dirt height and add the dirt noise on top. Then we cycle through every chunk in the column adding the block that matches using some if...else statements.

Lets try this out now by going to our World class and here in the CreateChunk function remove that for loop that adds blocks and instead add a call to our new class:

     public void CreateChunk(int x, int y, int z)
     {
         WorldPos worldPos = new WorldPos(x, y, z);
         //Instantiate the chunk at the coordinates using the chunk prefab
         GameObject newChunkObject = Instantiate(
                         chunkPrefab, new Vector3(x, y, z),
                         Quaternion.Euler(Vector3.zero)
                     ) as GameObject;
         Chunk newChunk = newChunkObject.GetComponent<Chunk>();
         newChunk.pos = worldPos;
         newChunk.world = this;
         //Add it to the chunks dictionary with the position as the key
         chunks.Add(worldPos, newChunk);
// REMOVE THIS
//        for (int xi = 0; xi < 16; xi++)
//        {
//            for (int yi = 0; yi < 16; yi++)
//            {
//                for (int zi = 0; zi < 16; zi++)
//                {
//                    if (yi <= 7)
//                    {
//                        SetBlock(x + xi, y + yi, z + zi, new BlockGrass());
//                    }
//                    else
//                    {
//                        SetBlock(x + xi, y + yi, z + zi, new BlockAir());
//                    }
//                }
//            }
//        }
//REMOVE ABOVE
         //Add these lines:
         var terrainGen = new TerrainGen();
         newChunk = terrainGen.ChunkGen(newChunk);
         newChunk.SetBlocksUnmodified();
         bool loaded = Serialization.Load(newChunk);
     }

Now before we test it you'll want to replace the Start function in world to have more chunks so you can see the terrain, replace it with this:

     // Use this for initialization
     void Start () {
         for (int x = -4; x < 4; x++)
         {
             for (int y = -1; y < 3; y++)
             {
                 for (int z = -4; z < 4; z++)
                 {
                     CreateChunk(x * 16, y * 16, z * 16);
                 }
             }
         }
     }

Now open unity and hit play to see your terrain generated!

[image]../../images/terrain.png[/image]

You should see something like this!

Tutorial part 5Download scripts so farTutorial part 7

24th Jan, 2015
Alex