Voxel Terrains

It looks like everybody wants to make a blocky world game like minecraft, so I decided to jump on the bandwagon and create my own voxel terrain script.

256x128x256 World Demo

First I decided to make it chunk based like most of the other implementations, but then I switched to an other method that just generates the biggest possible mesh everytime. This actually made the generation of the world quite slow. The resulting framerate was quite good considering it was pushing around 2.3 million vertices to the screen. This was with a 256x256x256 world. When I started looking for the reason why the generation was slow I found out that the culprit was mesh.Optimize(). To be honest it wasn’t that strange that an optimize would take a while on a big mesh. When I removed that line, the generation was way faster and much to my surprise, the resulting framerate was also higher.

Here is the code I used:

World.cs

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class World : MonoBehaviour
{

  public Vector3 size = new Vector3 (16, 16, 16);
  public Vector3 chunkSize = new Vector3 (16, 16, 16);
  public Vector3 blockSize = new Vector3 (1, 1, 1);

  public Material chunkMaterial;

  private Vectori3 worldSize;
  private Vectori3 worldChunkSize;

  private Chunk[,,] chunks;

  public BlockInfo[] blocks;

  public Perlin perlin;

  private int[] data;
  
  private float generatingPercentage = 0;

  // Use this for initialization
  void Start ()
  {
    worldSize = new Vectori3 ((int)size.x, (int)size.y, (int)size.z);
    worldChunkSize = new Vectori3 ((int)chunkSize.x, (int)chunkSize.y, (int)chunkSize.z);
    
    perlin = new Perlin ();
    
    StartCoroutine (meshSplitMethod ());
  }
  
  int calcIndex(int x, int y, int z)
  {
    return x + y*worldSize.x + z*worldSize.x*worldSize.y;
  }
  
  // this generates meshes of 65000 vertices 
  IEnumerator meshSplitMethod ()
  {
    data = new int[worldSize.x*worldSize.y*worldSize.z];
    generatingPercentage = 0;
    // generate data
    for (int x = 0; x < worldSize.x; x++) {
      for (int y = 0; y < worldSize.y; y++) {
        for (int z = 0; z < worldSize.z; z++) {
          data[x + y*worldSize.x + z*worldSize.x*worldSize.y] = (byte)Mathf.RoundToInt(Mathf.Clamp (perlin.Noise (x / 100.0f, y / 100.0f, z / 100.0f) * 3, 0, 1) * (blocks.Length - 1));
          //data[x, y, z] = 1;
        }
      }
      generatingPercentage = ((float)x/(float)worldSize.x)*100;
      yield return null;
    }
    
    generatingPercentage = 0;
    // create meshes
    List<Vector3> vertices = new List<Vector3> ();
    List<Color> colors = new List<Color> ();
    List<int> indices = new List<int> ();
    
    GameObject go = new GameObject ();
    Mesh mesh = new Mesh ();
    go.AddComponent<MeshFilter> ().sharedMesh = mesh;
    go.AddComponent<MeshRenderer> ();
    go.renderer.material = chunkMaterial;
    
    int blockIndex = 0;
    for (int x = 0; x < worldSize.x; x++) {
      for (int y = 0; y < worldSize.y; y++) {
        for (int z = 0; z < worldSize.z; z++) {
          blockIndex++;
          if (blocks[data[x + y*worldSize.x + z*worldSize.x*worldSize.y]].transparent)
            continue;
          
          // create block
          createBlock(x,y,z,vertices, colors, indices);
          
          if (vertices.Count > 65000) {
            mesh.vertices = vertices.ToArray ();
            mesh.colors = colors.ToArray ();
            mesh.triangles = indices.ToArray ();
            
            mesh.RecalculateBounds();
            mesh.RecalculateNormals();
            //mesh.Optimize();
            
            vertices.Clear ();
            colors.Clear ();
            indices.Clear ();
            
            // create new gameobject and mesh
            go = new GameObject ();
            mesh = new Mesh ();
            go.AddComponent<MeshFilter> ().sharedMesh = mesh;
            go.AddComponent<MeshRenderer> ();
            go.renderer.material = chunkMaterial;
            
            generatingPercentage = ((float)(blockIndex)/(float)(worldSize.x*worldSize.y*worldSize.z))*100;
            yield return null;
          }
        }
      }
    }
    //set last batch
    mesh.vertices = vertices.ToArray ();
    mesh.colors = colors.ToArray ();
    mesh.triangles = indices.ToArray ();
            
    mesh.RecalculateBounds();
    mesh.RecalculateNormals();
    generatingPercentage = 100;
    //mesh.Optimize();
  }
  
  Color getColor(int index)
  {
    return blocks[index].color;
  }
  
  // used in the split mesh method
  void createBlock (int x, int y, int z, List<Vector3> vertices, List<Color> colors, List<int> indices)
  {
    int currentIndex = vertices.Count;
    
    // bottom
    if (blocks[getBlockDirect(x, y + 1, z)].transparent) {
      vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z + blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z + blockSize.z));
      
      Color c = getColor(getBlockDirect(x, y, z));
      colors.Add (c);
      colors.Add (c);
      colors.Add (c);
      colors.Add (c);
      
      indices.Add (currentIndex + 0);
      indices.Add (currentIndex + 2);
      indices.Add (currentIndex + 1);
      indices.Add (currentIndex + 0);
      indices.Add (currentIndex + 3);
      indices.Add (currentIndex + 2);
      
      currentIndex += 4;
    }
    
    // top
    if (blocks[getBlockDirect (x, y - 1, z)].transparent) {
      vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y, z * blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y, z * blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y, z * blockSize.z + blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y, z * blockSize.z + blockSize.z));
      
      Color c = getColor(getBlockDirect(x, y, z));
      colors.Add (c);
      colors.Add (c);
      colors.Add (c);
      colors.Add (c);
      
      indices.Add (currentIndex + 0);
      indices.Add (currentIndex + 1);
      indices.Add (currentIndex + 2);
      indices.Add (currentIndex + 0);
      indices.Add (currentIndex + 2);
      indices.Add (currentIndex + 3);
      
      currentIndex += 4;
    }
    
    // left
    if (blocks[getBlockDirect (x - 1, y, z)].transparent) {
      vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y, z * blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z + blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y, z * blockSize.z + blockSize.z));
      
      Color c = getColor(getBlockDirect(x, y, z));
      colors.Add (c);
      colors.Add (c);
      colors.Add (c);
      colors.Add (c);
      
      indices.Add (currentIndex + 0);
      indices.Add (currentIndex + 2);
      indices.Add (currentIndex + 1);
      indices.Add (currentIndex + 0);
      indices.Add (currentIndex + 3);
      indices.Add (currentIndex + 2);
      
      currentIndex += 4;
    }
    
    // right
    if (blocks[getBlockDirect (x + 1, y, z)].transparent) {
      vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y, z * blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z + blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y, z * blockSize.z + blockSize.z));
      
      Color c = getColor(getBlockDirect(x, y, z));
      colors.Add (c);
      colors.Add (c);
      colors.Add (c);
      colors.Add (c);
      
      indices.Add (currentIndex + 0);
      indices.Add (currentIndex + 1);
      indices.Add (currentIndex + 2);
      indices.Add (currentIndex + 0);
      indices.Add (currentIndex + 2);
      indices.Add (currentIndex + 3);
      
      currentIndex += 4;
    }
    
    // front
    if (blocks[getBlockDirect (x, y, z - 1)].transparent) {
      vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y, z * blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y, z * blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z));
      
      Color c = getColor(getBlockDirect(x, y, z));
      colors.Add (c);
      colors.Add (c);
      colors.Add (c);
      colors.Add (c);
      
      indices.Add (currentIndex + 0);
      indices.Add (currentIndex + 2);
      indices.Add (currentIndex + 1);
      indices.Add (currentIndex + 0);
      indices.Add (currentIndex + 3);
      indices.Add (currentIndex + 2);
      
      currentIndex += 4;
    }
    
    // back
    if (blocks[getBlockDirect (x, y, z + 1)].transparent) {
      vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y, z * blockSize.z + blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y, z * blockSize.z + blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z + blockSize.z));
      vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z + blockSize.z));
      
      Color c = getColor(getBlockDirect(x, y, z));
      colors.Add (c);
      colors.Add (c);
      colors.Add (c);
      colors.Add (c);
      
      indices.Add (currentIndex + 0);
      indices.Add (currentIndex + 1);
      indices.Add (currentIndex + 2);
      indices.Add (currentIndex + 0);
      indices.Add (currentIndex + 2);
      indices.Add (currentIndex + 3);
      
      currentIndex += 4;
    }
  }
  
  // used in the split mesh method
  int getBlockDirect(int x, int y, int z)
  {
    // check if in bounds    
    if (x >= 0 && y >= 0 && z >= 0 && x < worldSize.x && y < worldSize.y && z < worldSize.z) {
      return data[x + y*worldSize.x + z*worldSize.x*worldSize.y];
    } else
      return 0;
  }
  
  // this splits the voxels up in chunks of the same size with each chunk having its own mesh.
  IEnumerator chunkMethod ()
  {
    // create chunks
    chunks = new Chunk[worldSize.x / worldChunkSize.x, worldSize.y / worldChunkSize.y, worldSize.z / worldChunkSize.z];
    
    // generate data
    for (int x = 0; x < worldSize.x / worldChunkSize.x; x++) {
      for (int y = 0; y < worldSize.y / worldChunkSize.y; y++) {
        for (int z = 0; z < worldSize.z / worldChunkSize.z; z++) {
          //Debug.Log("Creating chunk: "+x+","+y+","+z);
          chunks[x, y, z] = new Chunk (this, new Vectori3 (x, y, z), worldChunkSize, blockSize, chunkMaterial);
          // yield so it won't block the main thread
          //yield return null;
        }
      }
      yield return null;
    }
    
    //build mesh
    for (int x = 0; x < worldSize.x / worldChunkSize.x; x++) {
      for (int y = 0; y < worldSize.y / worldChunkSize.y; y++) {
        for (int z = 0; z < worldSize.z / worldChunkSize.z; z++) {
          //Debug.Log("Creating chunk mesh: "+x+","+y+","+z);
          chunks[x, y, z].updateMesh ();
          
        }
        // yield so it won't block the main thread
        yield return null;
      }
    }
    
  }
  
  // For use with the chunk method
  public int getBlock (Vectori3 position)
  {
    // check if in bounds    
    if (position.x >= 0 && position.y >= 0 && position.z >= 0 && position.x < worldSize.x && position.y < worldSize.y && position.z < worldSize.z) {
      // find correct chunk index
      Vectori3 chunk = position / worldChunkSize;
      Vectori3 pos = position - (chunk * worldChunkSize);
      return chunks[chunk.x, chunk.y, chunk.z].getBlock (pos.x, pos.y, pos.z);
    } else
      return 0;
    
  }

  public void explode (Vectori3 position, int radius)
  {
    // remove blocks in certain radius
    // maximum radius is chunksize - 1. This way only 4 chunks need to updated at most.
    
    // find the meshes that have blocks in them that are deleted or are touching blocks that are deleted
    // per mesh: remove vertices and indices that are not needed anymore
    // per mesh: check blocks surrounding the 
  }
  
  void OnGUI()
  {
    GUILayout.Label("Generating: " + generatingPercentage + "%");
  }
}

									

Chunk.cs

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Chunk
{
  private World world;
  private Vectori3 size;
  private Vector3 blockSize;
  private Vectori3 position;
  private GameObject go;
  private Mesh mesh;

  private byte[] data;

  private Material material;

  public Chunk (World world, Vectori3 position, Vectori3 size, Vector3 blockSize, Material material)
  {
    this.world = world;
    this.position = position;
    this.size = size;
    this.blockSize = blockSize;
    
    // create data array
    data = new byte[(size.x * size.y * size.z)];
    for (int i = 0; i < data.Length; i++) {
      int t = i % (size.x * size.y);
      int x = t % size.y;
      int y = t / size.y;
      int z = i / (size.x * size.y);
      
      float d = Mathf.Clamp (world.perlin.Noise ((x + position.x * size.x) / 100.0f, (y + position.y * size.y) / 100.0f, (z + position.z * size.z) / 100.0f) * 3, 0, 1) * (world.blocks.Length - 1);
      data[i] = (byte)Mathf.RoundToInt (d);
      //data[i] = (byte)Random.Range(1,world.blocks.Length);
      //data[i] = 1;
    }
    
    // create mesh
    mesh = new Mesh ();
    //updateMesh();
    
    // create gameobject
    go = new GameObject ();
    go.AddComponent<MeshFilter> ().sharedMesh = mesh;
    go.AddComponent<MeshRenderer> ();
    go.renderer.material = material;
    go.transform.position = new Vector3 (position.x * size.x * blockSize.x, position.y * size.y * blockSize.y, position.z * size.z * blockSize.z);
  }

  private Color getColor (int blockId)
  {
    return world.blocks[blockId].color;
  }

  public int getBlock (int x, int y, int z)
  {
    if (x >= 0 && y >= 0 && z >= 0 && x < size.x && y < size.y && z < size.z)
      return data[z * (size.x * size.y) + y * size.x + x];
    else
      // check neighbouring chunk
      return world.getBlock (position * size + new Vectori3 (x, y, z));
  }

  public void updateMesh ()
  {
    // update the mesh
    mesh.Clear ();
    
    List<Vector3> vertices = new List<Vector3> ();
    List<Color> colors = new List<Color> ();
    List<int> indices = new List<int> ();
    
    int currentIndex = 0;
    
    // iterate over data
    for (int i = 0; i < data.Length; i++) {
      if (!world.blocks[data[i]].transparent) {
        // create the required polygons
        int t = i % (size.x * size.y);
        int x = t % size.y;
        int y = t / size.y;
        int z = i / (size.x * size.y);
        
        // bottom
        if (world.blocks[getBlock (x, y + 1, z)].transparent) {
          vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z + blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z + blockSize.z));
          
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          
          indices.Add (currentIndex + 0);
          indices.Add (currentIndex + 2);
          indices.Add (currentIndex + 1);
          indices.Add (currentIndex + 0);
          indices.Add (currentIndex + 3);
          indices.Add (currentIndex + 2);
          
          currentIndex += 4;
        }
        
        // top
        if (world.blocks[getBlock (x, y - 1, z)].transparent) {
          vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y, z * blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y, z * blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y, z * blockSize.z + blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y, z * blockSize.z + blockSize.z));
          
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          
          indices.Add (currentIndex + 0);
          indices.Add (currentIndex + 1);
          indices.Add (currentIndex + 2);
          indices.Add (currentIndex + 0);
          indices.Add (currentIndex + 2);
          indices.Add (currentIndex + 3);
          
          currentIndex += 4;
        }
        
        // left
        if (world.blocks[getBlock (x - 1, y, z)].transparent) {
          vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y, z * blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z + blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y, z * blockSize.z + blockSize.z));
          
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          
          indices.Add (currentIndex + 0);
          indices.Add (currentIndex + 2);
          indices.Add (currentIndex + 1);
          indices.Add (currentIndex + 0);
          indices.Add (currentIndex + 3);
          indices.Add (currentIndex + 2);
          
          currentIndex += 4;
        }
        
        // right
        if (world.blocks[getBlock (x + 1, y, z)].transparent) {
          vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y, z * blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z + blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y, z * blockSize.z + blockSize.z));
          
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          
          indices.Add (currentIndex + 0);
          indices.Add (currentIndex + 1);
          indices.Add (currentIndex + 2);
          indices.Add (currentIndex + 0);
          indices.Add (currentIndex + 2);
          indices.Add (currentIndex + 3);
          
          currentIndex += 4;
        }
        
        // front
        if (world.blocks[getBlock (x, y, z - 1)].transparent) {
          vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y, z * blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y, z * blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z));
          
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          
          indices.Add (currentIndex + 0);
          indices.Add (currentIndex + 2);
          indices.Add (currentIndex + 1);
          indices.Add (currentIndex + 0);
          indices.Add (currentIndex + 3);
          indices.Add (currentIndex + 2);
          
          currentIndex += 4;
        }
        
        // back
        if (world.blocks[getBlock (x, y, z + 1)].transparent) {
          vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y, z * blockSize.z + blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y, z * blockSize.z + blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x + blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z + blockSize.z));
          vertices.Add (new Vector3 (x * blockSize.x, y * blockSize.y + blockSize.y, z * blockSize.z + blockSize.z));
          
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          colors.Add (getColor (getBlock (x, y, z)));
          
          indices.Add (currentIndex + 0);
          indices.Add (currentIndex + 1);
          indices.Add (currentIndex + 2);
          indices.Add (currentIndex + 0);
          indices.Add (currentIndex + 2);
          indices.Add (currentIndex + 3);
          
          currentIndex += 4;
        }
      }
    }
    
    mesh.vertices = vertices.ToArray ();
    mesh.colors = colors.ToArray ();
    mesh.triangles = indices.ToArray ();
    
    mesh.RecalculateBounds ();
    mesh.RecalculateNormals ();
    //mesh.Optimize();
  }
}

									

And the shader I used:

Shader "Custom/UnlitColor" {
  Properties {
    
  }
  SubShader {
    Tags { "RenderType"="Opaque" }
    LOD 200
    
    pass
    {
      CGPROGRAM
      #pragma vertex vert
      #pragma fragment frag
      #include "UnityCG.cginc"
  
      struct VIn {
        float4 vertex : POSITION;
        float3 normal : NORMAL;
        float4 color : COLOR;
      };
      struct VOut {
        float4 vertex : SV_POSITION;
        float4 color : COLOR;
      };
  
      VOut vert (VIn vin) {
        VOut result;
        result.vertex = mul(UNITY_MATRIX_MVP, vin.vertex);
        
        // multiply with the view normal to generate a lighting effect;
        // This generates a diffuse directional light aiming in the direction of the camera
        float d = max(dot(vin.normal, UNITY_MATRIX_IT_MV[2].xyz),0);
        
        // add an ambient factor
        d+=float4(0.1,0.1,0.1,1);
        
        result.color = vin.color*d;
        return result;
      }
      
      float4 frag(VOut fin) : COLOR
      {
        return fin.color;
      }
      ENDCG
    }
  } 
  FallBack "Diffuse"
}

									

2 thoughts on “Voxel Terrains

Leave a Reply

Your email address will not be published. Required fields are marked *