For fun I am trying to make a voxel meshing script that is fast and memory efficient. The current code works well at memory management (no leaks) and is fairly fast but the time needed to generate an entire chunk (16 sub chunks) still is anywhere between 10-20ms.
Sub-Chunk Code
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using UnityEngine.Profiling;
[RequireComponent(typeof(MeshFilter)), RequireComponent(typeof(MeshRenderer)), RequireComponent(typeof(MeshCollider))]
public class SubChunk : MonoBehaviour
{
public Chunk chunk;
public World world;
public int height;
private MeshFilter meshFilter;
private MeshRenderer meshRenderer;
private MeshCollider meshCollider;
private Mesh mesh;
// Declared here to help with optimization of the lists and GC
private static Vector3[] northNormals = { Vector3.right, Vector3.right, Vector3.right, Vector3.right };
private static Vector3[] southNormals = { Vector3.left, Vector3.left, Vector3.left, Vector3.left };
private static Vector3[] eastNormals = { Vector3.back, Vector3.back, Vector3.back, Vector3.back };
private static Vector3[] westNormals = { Vector3.forward, Vector3.forward, Vector3.forward, Vector3.forward };
private static Vector3[] upNormals = { Vector3.up, Vector3.up, Vector3.up, Vector3.up };
private static Vector3[] downNormals = { Vector3.down, Vector3.down, Vector3.down, Vector3.down };
public void Awake()
{
meshFilter = gameObject.GetComponent<MeshFilter>();
meshRenderer = gameObject.GetComponent<MeshRenderer>();
meshCollider = gameObject.GetComponent<MeshCollider>();
mesh = new Mesh();
meshFilter.sharedMesh = mesh;
}
public void GenerateMesh()
{
Vector3[] vertices = new Vector3[19652];
Vector3[] normals = new Vector3[19652];
Vector2[] uv = new Vector2[19652];
int[] triangles = new int[29478];
BlockData.Material self;
BlockData.Material north;
BlockData.Material south;
BlockData.Material east;
BlockData.Material west;
BlockData.Material up;
BlockData.Material down;
int vertexCount = 0;
for (int x = 0; x < 16; x++)
{
for (int y = 0; y < 16; y++)
{
for (int z = 0; z < 16; z++)
{
self = chunk.GetChunkBlock(x, y + height, z);
north = chunk.GetChunkBlock(x + 1, y + height, z);
south = chunk.GetChunkBlock(x - 1, y + height, z);
east = chunk.GetChunkBlock(x, y + height, z - 1);
west = chunk.GetChunkBlock(x, y + height, z + 1);
up = chunk.GetChunkBlock(x, y + 1 + height, z);
down = chunk.GetChunkBlock(x, y - 1 + height, z);
// Only render faces if the block is transparent
if (BlockData.IsBlock(self))
{
if (ShouldRenderFace(self, north))
{
// Make north quad
// Add verts
vertices[vertexCount] = (new Vector3(x - 7.5f, y - 8.5f, z - 8.5f));
vertices[vertexCount + 1] = (new Vector3(x - 7.5f, y - 7.5f, z - 8.5f));
vertices[vertexCount + 2] = (new Vector3(x - 7.5f, y - 7.5f, z - 7.5f));
vertices[vertexCount + 3] = (new Vector3(x - 7.5f, y - 8.5f, z - 7.5f));
// Add normals
northNormals.CopyTo(normals, vertexCount);
// Add uvs
uv[vertexCount] = new Vector2(0, 0);
uv[vertexCount + 1] = new Vector2(0, 1);
uv[vertexCount + 2] = new Vector2(1, 1);
uv[vertexCount + 3] = new Vector2(1, 0);
triangles[vertexCount / 4 * 6] = vertexCount;
triangles[vertexCount / 4 * 6 + 1] = vertexCount + 1;
triangles[vertexCount / 4 * 6 + 2] = vertexCount + 2;
triangles[vertexCount / 4 * 6 + 3] = vertexCount;
triangles[vertexCount / 4 * 6 + 4] = vertexCount + 2;
triangles[vertexCount / 4 * 6 + 5] = vertexCount + 3;
// Add tris
vertexCount += 4;
}
if (ShouldRenderFace(self, south))
{
// Make south quad
// Add verts
vertices[vertexCount] = (new Vector3(x - 8.5f, y - 8.5f, z - 7.5f));
vertices[vertexCount + 1] = (new Vector3(x - 8.5f, y - 7.5f, z - 7.5f));
vertices[vertexCount + 2] = (new Vector3(x - 8.5f, y - 7.5f, z - 8.5f));
vertices[vertexCount + 3] = (new Vector3(x - 8.5f, y - 8.5f, z - 8.5f));
// Add normals
southNormals.CopyTo(normals, vertexCount);
// Add uvs
uv[vertexCount] = new Vector2(0, 0);
uv[vertexCount + 1] = new Vector2(0, 1);
uv[vertexCount + 2] = new Vector2(1, 1);
uv[vertexCount + 3] = new Vector2(1, 0);
// Add tris
triangles[vertexCount / 4 * 6] = vertexCount;
triangles[vertexCount / 4 * 6 + 1] = vertexCount + 1;
triangles[vertexCount / 4 * 6 + 2] = vertexCount + 2;
triangles[vertexCount / 4 * 6 + 3] = vertexCount;
triangles[vertexCount / 4 * 6 + 4] = vertexCount + 2;
triangles[vertexCount / 4 * 6 + 5] = vertexCount + 3;
vertexCount += 4;
}
if (ShouldRenderFace(self, east))
{
// Make east quad
// Add verts
vertices[vertexCount] = (new Vector3(x - 8.5f, y - 8.5f, z - 8.5f));
vertices[vertexCount + 1] = (new Vector3(x - 8.5f, y - 7.5f, z - 8.5f));
vertices[vertexCount + 2] = (new Vector3(x - 7.5f, y - 7.5f, z - 8.5f));
vertices[vertexCount + 3] = (new Vector3(x - 7.5f, y - 8.5f, z - 8.5f));
// Add normals
eastNormals.CopyTo(normals, vertexCount);
// Add uvs
uv[vertexCount] = new Vector2(0, 0);
uv[vertexCount + 1] = new Vector2(0, 1);
uv[vertexCount + 2] = new Vector2(1, 1);
uv[vertexCount + 3] = new Vector2(1, 0);
// Add tris
triangles[vertexCount / 4 * 6] = vertexCount;
triangles[vertexCount / 4 * 6 + 1] = vertexCount + 1;
triangles[vertexCount / 4 * 6 + 2] = vertexCount + 2;
triangles[vertexCount / 4 * 6 + 3] = vertexCount;
triangles[vertexCount / 4 * 6 + 4] = vertexCount + 2;
triangles[vertexCount / 4 * 6 + 5] = vertexCount + 3;
vertexCount += 4;
}
if (ShouldRenderFace(self, west))
{
// Make west quad
// Add verts
vertices[vertexCount] = (new Vector3(x - 7.5f, y - 8.5f, z - 7.5f));
vertices[vertexCount + 1] = (new Vector3(x - 7.5f, y - 7.5f, z - 7.5f));
vertices[vertexCount + 2] = (new Vector3(x - 8.5f, y - 7.5f, z - 7.5f));
vertices[vertexCount + 3] = (new Vector3(x - 8.5f, y - 8.5f, z - 7.5f));
// Add normals
westNormals.CopyTo(normals, vertexCount);
// Add uvs
uv[vertexCount] = new Vector2(0, 0);
uv[vertexCount + 1] = new Vector2(0, 1);
uv[vertexCount + 2] = new Vector2(1, 1);
uv[vertexCount + 3] = new Vector2(1, 0);
// Add tris
triangles[vertexCount / 4 * 6] = vertexCount;
triangles[vertexCount / 4 * 6 + 1] = vertexCount + 1;
triangles[vertexCount / 4 * 6 + 2] = vertexCount + 2;
triangles[vertexCount / 4 * 6 + 3] = vertexCount;
triangles[vertexCount / 4 * 6 + 4] = vertexCount + 2;
triangles[vertexCount / 4 * 6 + 5] = vertexCount + 3;
vertexCount += 4;
}
if (ShouldRenderFace(self, up))
{
// Make up quad
// Add verts
vertices[vertexCount] = (new Vector3(x - 8.5f, y - 7.5f, z - 7.5f));
vertices[vertexCount + 1] = (new Vector3(x - 7.5f, y - 7.5f, z - 7.5f));
vertices[vertexCount + 2] = (new Vector3(x - 7.5f, y - 7.5f, z - 8.5f));
vertices[vertexCount + 3] = (new Vector3(x - 8.5f, y - 7.5f, z - 8.5f));
// Add normals
upNormals.CopyTo(normals, vertexCount);
// Add uvs
uv[vertexCount] = new Vector2(0, 0);
uv[vertexCount + 1] = new Vector2(0, 1);
uv[vertexCount + 2] = new Vector2(1, 1);
uv[vertexCount + 3] = new Vector2(1, 0);
// Add tris
triangles[vertexCount / 4 * 6] = vertexCount;
triangles[vertexCount / 4 * 6 + 1] = vertexCount + 1;
triangles[vertexCount / 4 * 6 + 2] = vertexCount + 2;
triangles[vertexCount / 4 * 6 + 3] = vertexCount;
triangles[vertexCount / 4 * 6 + 4] = vertexCount + 2;
triangles[vertexCount / 4 * 6 + 5] = vertexCount + 3;
vertexCount += 4;
}
if (ShouldRenderFace(self, down))
{
// Make down quad
// Add verts
vertices[vertexCount] = (new Vector3(x - 7.5f, y - 8.5f, z - 7.5f));
vertices[vertexCount + 1] = (new Vector3(x - 8.5f, y - 8.5f, z - 7.5f));
vertices[vertexCount + 2] = (new Vector3(x - 8.5f, y - 8.5f, z - 8.5f));
vertices[vertexCount + 3] = (new Vector3(x - 7.5f, y - 8.5f, z - 8.5f));
// Add normals
downNormals.CopyTo(normals, vertexCount);
// Add uvs
uv[vertexCount] = new Vector2(0, 0);
uv[vertexCount + 1] = new Vector2(0, 1);
uv[vertexCount + 2] = new Vector2(1, 1);
uv[vertexCount + 3] = new Vector2(1, 0);
// Add tris
triangles[vertexCount / 4 * 6] = vertexCount;
triangles[vertexCount / 4 * 6 + 1] = vertexCount + 1;
triangles[vertexCount / 4 * 6 + 2] = vertexCount + 2;
triangles[vertexCount / 4 * 6 + 3] = vertexCount;
triangles[vertexCount / 4 * 6 + 4] = vertexCount + 2;
triangles[vertexCount / 4 * 6 + 5] = vertexCount + 3;
vertexCount += 4;
}
}
}
}
}
mesh.Clear();
Vector3[] finalVertices = new Vector3[vertexCount];
Vector3[] finalNormals = new Vector3[vertexCount];
Vector2[] finalUV = new Vector2[vertexCount];
int[] finalTriangles= new int[vertexCount / 4 * 6];
for (int i = 0; i < vertexCount; i++)
{
finalVertices[i] = vertices[i];
finalNormals[i] = normals[i];
finalUV[i] = uv[i];
}
for (int i = 0; i < vertexCount / 4 * 6; i++)
{
finalTriangles[i] = triangles[i];
}
mesh.vertices = finalVertices;
mesh.normals = finalNormals;
mesh.uv = finalUV;
mesh.triangles = finalTriangles;
mesh.Optimize();
meshCollider.sharedMesh = mesh;
}
private bool ShouldRenderFace(BlockData.Material self, BlockData.Material relitive)
{
return (!BlockData.IsBlock(relitive) || (!BlockData.IsTransparent(self) && BlockData.IsTransparent(relitive)));
}
}
Chunk Code
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;
public class Chunk : MonoBehaviour
{
public BlockData.Material[,,] chunkData = new BlockData.Material[16,256,16];
public int chunkX = 0;
public int chunkZ = 0;
private World world;
public SubChunk subChunk_0;
public SubChunk subChunk_1;
public SubChunk subChunk_2;
public SubChunk subChunk_3;
public SubChunk subChunk_4;
public SubChunk subChunk_5;
public SubChunk subChunk_6;
public SubChunk subChunk_7;
public SubChunk subChunk_8;
public SubChunk subChunk_9;
public SubChunk subChunk_10;
public SubChunk subChunk_11;
public SubChunk subChunk_12;
public SubChunk subChunk_13;
public SubChunk subChunk_14;
public SubChunk subChunk_15;
public void Awake()
{
subChunk_0.chunk = this;
subChunk_1.chunk = this;
subChunk_2.chunk = this;
subChunk_3.chunk = this;
subChunk_4.chunk = this;
subChunk_5.chunk = this;
subChunk_6.chunk = this;
subChunk_7.chunk = this;
subChunk_8.chunk = this;
subChunk_9.chunk = this;
subChunk_10.chunk = this;
subChunk_11.chunk = this;
subChunk_12.chunk = this;
subChunk_13.chunk = this;
subChunk_14.chunk = this;
subChunk_15.chunk = this;
subChunk_0.height = 0 * 16;
subChunk_1.height = 1 * 16;
subChunk_2.height = 2 * 16;
subChunk_3.height = 3 * 16;
subChunk_4.height = 4 * 16;
subChunk_5.height = 5 * 16;
subChunk_6.height = 6 * 16;
subChunk_7.height = 7 * 16;
subChunk_8.height = 8 * 16;
subChunk_9.height = 9 * 16;
subChunk_10.height = 10 * 16;
subChunk_11.height = 11 * 16;
subChunk_12.height = 12 * 16;
subChunk_13.height = 13 * 16;
subChunk_14.height = 14 * 16;
subChunk_15.height = 15 * 16;
}
public void setWorld(World world)
{
this.world = world;
subChunk_0.world = world;
subChunk_1.world = world;
subChunk_2.world = world;
subChunk_3.world = world;
subChunk_4.world = world;
subChunk_5.world = world;
subChunk_6.world = world;
subChunk_7.world = world;
subChunk_8.world = world;
subChunk_9.world = world;
subChunk_10.world = world;
subChunk_11.world = world;
subChunk_12.world = world;
subChunk_13.world = world;
subChunk_14.world = world;
subChunk_15.world = world;
}
public void RenderChunk()
{
System.Diagnostics.Stopwatch stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
subChunk_0.GenerateMesh();
subChunk_1.GenerateMesh();
subChunk_2.GenerateMesh();
subChunk_3.GenerateMesh();
subChunk_4.GenerateMesh();
subChunk_5.GenerateMesh();
subChunk_6.GenerateMesh();
subChunk_7.GenerateMesh();
subChunk_8.GenerateMesh();
subChunk_9.GenerateMesh();
subChunk_10.GenerateMesh();
subChunk_11.GenerateMesh();
subChunk_12.GenerateMesh();
subChunk_13.GenerateMesh();
subChunk_14.GenerateMesh();
subChunk_15.GenerateMesh();
stopwatch.Stop();
Debug.Log(stopwatch.ElapsedMilliseconds);
}
public BlockData.Material GetChunkBlock(int x, int y, int z)
{
if (x >= 0 && x < 16)
{
if (y >= 0 && y < 256)
{
if (z >= 0 && z < 16)
{
return chunkData[x, y, z];
} else if (z < 0)
{
return world.GetBlockAt(chunkX, chunkZ - 1, x, y, 15);
} else
{
return world.GetBlockAt(chunkX, chunkZ + 1, x, y, 0);
}
}
} else if (x < 0)
{
return world.GetBlockAt(chunkX - 1, chunkZ, 15, y, z);
} else
{
return world.GetBlockAt(chunkX + 1, chunkZ, 0, y, z);
}
return BlockData.Material.AIR;
}
}
World Code
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class World : MonoBehaviour
{
public static int SEED = 15163;
public GameObject chunkPrefab;
private Dictionary<Vector2Int, Chunk> chunks = new Dictionary<Vector2Int, Chunk>();
public void Start()
{
for (int x = -8; x < 8; x++)
{
for (int z = -8; z < 8; z++)
{
GenerateChunk(x, z);
}
}
}
public void GenerateChunk(int chunkX, int chunkZ)
{
Vector2Int key = new Vector2Int(chunkX, chunkZ);
Vector2Int keyN = new Vector2Int(chunkX + 1, chunkZ);
Vector2Int keyS = new Vector2Int(chunkX - 1, chunkZ);
Vector2Int keyE = new Vector2Int(chunkX, chunkZ - 1);
Vector2Int keyW = new Vector2Int(chunkX, chunkZ + 1);
if (!chunks.ContainsKey(key))
{
GameObject chunkObject = Instantiate(chunkPrefab, transform);
chunkObject.transform.position = new Vector3(chunkX * 16, 0, chunkZ * 16);
Chunk chunk = chunkObject.GetComponent<Chunk>();
BlockData.Material[,,] blockData = new BlockData.Material[16, 256, 16];
// This is a temporary noise generator
for (int x = 0; x < 16; x++)
{
for (int z = 0; z < 16; z++)
{
float height = Mathf.PerlinNoise((x + SEED + chunkX * 16) / 30f, (z + SEED + chunkZ * 16) / 30f) * 20 + 64;
for (int y = 0; y < 256; y++)
{
if (height > y)
{
blockData[x, y, z] = BlockData.Material.STONE;
}
}
}
}
// End temporary noise generator
chunk.chunkData = blockData;
chunk.chunkX = chunkX;
chunk.chunkZ = chunkZ;
chunk.setWorld(this);
chunk.RenderChunk();
chunks.Add(key, chunk);
}
if (chunks.ContainsKey(keyN))
{
chunks[keyN].RenderChunk();
}
if (chunks.ContainsKey(keyS))
{
chunks[keyS].RenderChunk();
}
if (chunks.ContainsKey(keyE))
{
chunks[keyE].RenderChunk();
}
if (chunks.ContainsKey(keyW))
{
chunks[keyW].RenderChunk();
}
}
public BlockData.Material GetBlockAt(int chunkX, int chunkZ, int x, int y, int z)
{
Vector2Int key = new Vector2Int(chunkX, chunkZ);
if (chunks.ContainsKey(key))
{
if (x >= 0 && x < 16 && y >= 0 && y < 256 && z >= 0 && z < 16)
{
return chunks[key].chunkData[x, y, z];
} else
{
return BlockData.Material.AIR;
}
} else
{
return BlockData.Material.AIR;
}
}
}