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.
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"
}
Nice post, but it’s impossible to test without BlockInfo & Perlin class. 🙂
Thx btw!
In fact, no, Perlin class is into the procedural exemple of Unity. but not the BlockInfo class 🙂