- Home /
[Optimization] How can i optimize this voxel terrain more?
Hey there :)
so over the last few weeks i tried to optimize my terrain generation because it is still a bit too slow.
What i´ve done so far:
Create the visible faces using quads.
Combine the quads to a block
Create chunks of blocks
Combine all blocks in a chunk
i´ve tried adding some time in the coroutines to minimize the fps hit but this does slow down the generation too much and doesn´t seem to be the correct way of doing this.
I´d be grateful if someone has the time to take a look at the scripts :)
Thanks in advance.
Here are the main scripts
Generator
Chunk
CreateQuads
I am sorry if this is a bit too hard to read in this editor here.
Here is the main generator
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using LibNoise.Generator;
using Realtime.Messaging.Internal;
using TerrainGenerator;
using System.Threading;
public class Generator : MonoBehaviour
{
public static Generator generator;
public bool generateChunks = true;
public bool removeChunks = true;
public int removeDistanceInChunks;
public int renderDistance;
public int HeightmapResolution;
public int chunkHeight;
public Material cubeMaterial;
private Perlin perlinNoiseGenerator;
public Transform player;
public static ConcurrentDictionary<string, Chunk> chunks;
public double _frequency = 1.0;
public double _lacunarity = 2.0;
public int _octaveCount = 6;
public double _persistence = 0.5;
public int _seed;
public CoroutineQueue queue;
public static uint maxCoroutines = 1500;
public CoroutineQueue buildFinishQueue;
public static uint maxBuildFinishCoroutines = 1500;
public Vector3 lastbuildPos;
private Vector3 savedPlayerPos;
void Start()
{
generator = this;
Loom.Initialize();
player.gameObject.SetActive(false);
chunks = new ConcurrentDictionary<string, Chunk>();
player.position = new Vector3(0, 2, 0);
perlinNoiseGenerator = new Perlin();
queue = new CoroutineQueue(maxCoroutines, StartCoroutine);
buildFinishQueue = new CoroutineQueue(maxBuildFinishCoroutines, StartCoroutine);
perlinNoiseGenerator.Frequency = _frequency;
perlinNoiseGenerator.Lacunarity = _lacunarity;
perlinNoiseGenerator.OctaveCount = _octaveCount;
perlinNoiseGenerator.Persistence = _persistence;
perlinNoiseGenerator.Seed = _seed;
Generate();
}
void Update()
{
savedPlayerPos.x = player.position.x;
savedPlayerPos.z = player.position.z;
UpdateTerrain(savedPlayerPos, renderDistance);
}
public void Generate()
{
var newChunks = GetChunkPositionsInRadius(GetChunkPosition(new Vector3(0, 0, 0)), renderDistance);
foreach (var position in newChunks)
{
GenerateChunk(position.X, position.Z);
}
player.gameObject.SetActive(true);
}
public void UpdateTerrain(Vector3 worldPosition, int radius)
{
Vector3 movement = lastbuildPos - worldPosition;
if (movement.magnitude > HeightmapResolution)
{
queue.Run(BuildWorld(worldPosition, radius));
}
}
IEnumerator BuildWorld(Vector3 worldPosition, int radius)
{
lastbuildPos = worldPosition;
var newChunks = GetChunkPositionsInRadius(GetChunkPosition(worldPosition), radius);
StartCoroutine(GenerateChunkTimed(newChunks));
yield return new WaitForSeconds(0.2f);
if (removeChunks)
queue.Run(RemoveChunksOutOfRangeEnum(removeDistanceInChunks));
}
IEnumerator GenerateChunkTimed(List<Vector2i> newChunks)
{
foreach (var position in newChunks)
{
if (generateChunks)
{
if (!CheckObsoleteChunk(position.X, position.Z))
{
GenerateChunk(position.X, position.Z, true);
yield return new WaitForSeconds(0.2f);
}
}
}
}
IEnumerator RemoveChunksOutOfRangeEnum(int distanceInChunks)
{
foreach (var chunk in chunks)
{
float dist = Vector3.Distance(new Vector3(chunk.Value.Position.X, 0, chunk.Value.Position.Z), player.position);
if (dist < 0)
dist = dist * -1;
if (dist >= distanceInChunks * HeightmapResolution)
{
RemoveChunk(chunk.Value);
yield return null;
}
}
}
public Vector2i GetChunkPosition(Vector3 worldPosition)
{
var x = (int)Mathf.Floor(worldPosition.x / HeightmapResolution);
var z = (int)Mathf.Floor(worldPosition.z / HeightmapResolution);
return new Vector2i(x, z);
}
private List<Vector2i> GetChunkPositionsInRadius(Vector2i chunkPosition, int radius)
{
var result = new List<Vector2i>();
for (var zCircle = -radius; zCircle <= radius; zCircle++)
{
for (var xCircle = -radius; xCircle <= radius; xCircle++)
{
if (xCircle * xCircle + zCircle * zCircle < radius * radius)
result.Add(new Vector2i((chunkPosition.X + xCircle) * HeightmapResolution, (chunkPosition.Z + zCircle) * HeightmapResolution));
}
}
return result;
}
public bool CheckObsoleteChunk(int x, int z)
{
if (chunks.ContainsKey(x + ", " + z))
return true;
return false;
}
public void GenerateChunk(int x, int z, bool newGeneration = false)
{
if (!CheckObsoleteChunk(x, z))
chunks.TryAdd(x + ", " + z, new Chunk(HeightmapResolution, chunkHeight, x, z, cubeMaterial, perlinNoiseGenerator, newGeneration));
}
public void RemoveChunk(Chunk ch)
{
Chunk o;
Destroy(ch.spawnObject.gameObject);
chunks.TryRemove(ch.Position.X + ", " + ch.Position.Z, out o);
ch = null;
}
}
Chunk:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TerrainGenerator;
using System;
using LibNoise.Generator;
using System.Threading;
public class Chunk
{
public Transform spawnObject;
public Material cubeMaterial;
public Vector2i Position;
public float[,] Heightmap;
private List<Vector3> blockPos = new List<Vector3>();
public GameObject[] blocks;
public int HeightmapResolution;
private Perlin PerlinNoiseGenerator;
public int chunkHeight;
private static Generator generator = null;
private Thread generateHeightMapThread;
private bool newGeneration = false;
public Chunk(int HeightmapResolution, int cHeight, int x, int z, Material mat, Perlin p, bool newGeneration)
{
if (generator == null)
generator = Generator.generator;
spawnObject = new GameObject("Chunk").transform;
this.newGeneration = newGeneration;
spawnObject.position = new Vector3(x, 0, z);
chunkHeight = cHeight;
blocks = new GameObject[(HeightmapResolution * HeightmapResolution) * chunkHeight];
cubeMaterial = mat;
Position = new Vector2i(x, z);
this.HeightmapResolution = HeightmapResolution;
PerlinNoiseGenerator = p;
//generateHeightMapThread =
// new Thread(
// unused => GenerateHeightmapThread()
// );
//generateHeightMapThread.Start();
GenerateHeightmapThread();
}
private static float FastFloor(double f)
{
return (f >= 0.0f ? (int)f : (int)f - 1);
}
public void GenerateHeightmapThread()
{
var heightmap = new float[HeightmapResolution, HeightmapResolution];
for (var zRes = 0; zRes < HeightmapResolution; zRes++)
{
for (var xRes = 0; xRes < HeightmapResolution; xRes++)
{
var xCoordinate = Position.X + xRes;
var zCoordinate = Position.Z + zRes;
// float height = (float)Math.Floor(PerlinNoiseGenerator.GetValue(xCoordinate, 0, zCoordinate) * 10);
float height = FastFloor(PerlinNoiseGenerator.GetValue(xCoordinate, 0, zCoordinate) * 10);
if (height < 0)
height *= -1;
heightmap[zRes, xRes] = height;
}
}
Heightmap = heightmap;
// Loom.QueueOnMainThread(() =>
// {
if (newGeneration)
generator.queue.Run(BuildChunkE());
else
BuildChunk();
// });
}
public void BuildChunk()
{
// generateHeightMapThread.Abort();
int count = 0;
for (int z = 0; z < HeightmapResolution; z++)
{
for (int x = 0; x < HeightmapResolution; x++)
{
GameObject newBlock;
for (int i = 0; i < chunkHeight; i++)
{
newBlock = new GameObject("cube" + z + " " + x);
newBlock.AddComponent<CreateQuads>();
newBlock.GetComponent<CreateQuads>().owner = newBlock;
newBlock.GetComponent<CreateQuads>().cubeMaterial = cubeMaterial;
float y = Heightmap[z, x] - i;
if (y == 0)
newBlock.GetComponent<CreateQuads>().bType = CreateQuads.BlockType.GRASS;
else if (y == 1)
newBlock.GetComponent<CreateQuads>().bType = CreateQuads.BlockType.DIRT;
else
newBlock.GetComponent<CreateQuads>().bType = CreateQuads.BlockType.STONE;
count++;
newBlock.transform.parent = spawnObject.transform;
newBlock.transform.position = new Vector3(Position.X + x, y, Position.Z + z);
blocks[count - 1] = newBlock;
//blockPos.Add(new Vector3(newBlock.transform.position.x, newBlock.transform.position.y, newBlock.transform.position.z));
blockPos.Add(newBlock.transform.position);
}
}
}
foreach (GameObject block in blocks)
{
bool front, back, top, bottom, left, right;
front = back = top = bottom = left = right = false;
Vector3 position = block.transform.position;
if (!blockPos.Contains(new Vector3((int)position.x + 1, (int)position.y, (int)position.z)))
{
//DRAW
right = true;
}
if (!blockPos.Contains(new Vector3((int)position.x - 1, (int)position.y, (int)position.z)))
{
//DRAW
left = true;
}
if (!blockPos.Contains(new Vector3((int)position.x, (int)position.y + 1, (int)position.z)))
{
//DRAW
top = true;
}
//if (!blockPos.Contains(new Vector3((int)position.x, (int)position.y - 1, (int)position.z)))
//{
// //DRAW
// bottom = true;
//}
if (!blockPos.Contains(new Vector3((int)position.x, (int)position.y, (int)position.z + 1)))
{
//DRAW
front = true;
}
if (!blockPos.Contains(new Vector3((int)position.x, (int)position.y, (int)position.z - 1)))
{
//DRAW
back = true;
}
block.GetComponent<CreateQuads>().CreateCube(front, back, top, bottom, left, right);
}
spawnObject.gameObject.AddComponent<Combine>().cubeMaterial = cubeMaterial;
spawnObject.GetComponent<Combine>().NewCombine();
}
IEnumerator BuildChunkE()
{
int count = 0;
for (int z = 0; z < HeightmapResolution; z++)
{
for (int x = 0; x < HeightmapResolution; x++)
{
GameObject newBlock;
for (int i = 0; i < chunkHeight; i++)
{
newBlock = new GameObject("cube" + z + " " + x);
newBlock.AddComponent<CreateQuads>();
newBlock.GetComponent<CreateQuads>().owner = newBlock;
newBlock.GetComponent<CreateQuads>().cubeMaterial = cubeMaterial;
float y = Heightmap[z, x] - i;
if (y == 0)
newBlock.GetComponent<CreateQuads>().bType = CreateQuads.BlockType.GRASS;
else if (y == 1)
newBlock.GetComponent<CreateQuads>().bType = CreateQuads.BlockType.DIRT;
else
newBlock.GetComponent<CreateQuads>().bType = CreateQuads.BlockType.STONE;
count++;
newBlock.transform.parent = spawnObject.transform;
newBlock.transform.position = new Vector3(Position.X + x, y, Position.Z + z);
blocks[count - 1] = newBlock;
//blockPos.Add(new Vector3(newBlock.transform.position.x, newBlock.transform.position.y, newBlock.transform.position.z));
blockPos.Add(newBlock.transform.position);
}
}
}
yield return null;
foreach (GameObject block in blocks)
{
bool front, back, top, bottom, left, right;
front = back = top = bottom = left = right = false;
Vector3 position = block.transform.position;
if (!blockPos.Contains(new Vector3((int)position.x + 1, (int)position.y, (int)position.z)))
{
//DRAW
right = true;
}
if (!blockPos.Contains(new Vector3((int)position.x - 1, (int)position.y, (int)position.z)))
{
//DRAW
left = true;
}
if (!blockPos.Contains(new Vector3((int)position.x, (int)position.y + 1, (int)position.z)))
{
//DRAW
top = true;
}
//if (!blockPos.Contains(new Vector3((int)position.x, (int)position.y - 1, (int)position.z)))
//{
// //DRAW
// bottom = true;
//}
if (!blockPos.Contains(new Vector3((int)position.x, (int)position.y, (int)position.z + 1)))
{
//DRAW
front = true;
}
if (!blockPos.Contains(new Vector3((int)position.x, (int)position.y, (int)position.z - 1)))
{
//DRAW
back = true;
}
block.GetComponent<CreateQuads>().CreateCube(front, back, top, bottom, left, right);
}
generator.queue.Run(finishBuild());
}
public IEnumerator finishBuild()
{
yield return new WaitForSeconds(0.1f);
spawnObject.gameObject.AddComponent<Combine>().cubeMaterial = cubeMaterial;
yield return null;
spawnObject.GetComponent<Combine>().NewCombine();
}
}
And the last script CreateQuads
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CreateQuads : MonoBehaviour
{
public GameObject owner;
enum Cubeside { BOTTOM, TOP, LEFT, RIGHT, FRONT, BACK };
public enum BlockType { GRASS, DIRT, STONE };
public Material cubeMaterial;
public BlockType bType;
Vector2[,] blockUVs = {
/*GRASS TOP*/ {new Vector2( 0.125f, 0.375f ), new Vector2( 0.1875f, 0.375f),
new Vector2( 0.125f, 0.4375f ),new Vector2( 0.1875f, 0.4375f )},
/*GRASS SIDE*/ {new Vector2( 0.1875f, 0.9375f ), new Vector2( 0.25f, 0.9375f),
new Vector2( 0.1875f, 1.0f ),new Vector2( 0.25f, 1.0f )},
/*DIRT*/ {new Vector2( 0.125f, 0.9375f ), new Vector2( 0.1875f, 0.9375f),
new Vector2( 0.125f, 1.0f ),new Vector2( 0.1875f, 1.0f )},
/*STONE*/ {new Vector2( 0, 0.875f ), new Vector2( 0.0625f, 0.875f),
new Vector2( 0, 0.9375f ),new Vector2( 0.0625f, 0.9375f )}
};
void CreateQuad(Cubeside side)
{
Mesh mesh = new Mesh();
mesh.name = "ScriptedMesh" + side.ToString();
Vector3[] vertices = new Vector3[4];
Vector3[] normals = new Vector3[4];
Vector2[] uvs = new Vector2[4];
int[] triangles = new int[6];
//all possible UVs
Vector2 uv00;
Vector2 uv10;
Vector2 uv01;
Vector2 uv11;
if (bType == BlockType.GRASS && side == Cubeside.TOP)
{
uv00 = blockUVs[0, 0];
uv10 = blockUVs[0, 1];
uv01 = blockUVs[0, 2];
uv11 = blockUVs[0, 3];
}
else if (bType == BlockType.GRASS && side == Cubeside.BOTTOM)
{
uv00 = blockUVs[(int)(BlockType.DIRT + 1), 0];
uv10 = blockUVs[(int)(BlockType.DIRT + 1), 1];
uv01 = blockUVs[(int)(BlockType.DIRT + 1), 2];
uv11 = blockUVs[(int)(BlockType.DIRT + 1), 3];
}
else
{
uv00 = blockUVs[(int)(bType + 1), 0];
uv10 = blockUVs[(int)(bType + 1), 1];
uv01 = blockUVs[(int)(bType + 1), 2];
uv11 = blockUVs[(int)(bType + 1), 3];
}
//all possible vertices
Vector3 p0 = new Vector3(-0.5f, -0.5f, 0.5f);
Vector3 p1 = new Vector3(0.5f, -0.5f, 0.5f);
Vector3 p2 = new Vector3(0.5f, -0.5f, -0.5f);
Vector3 p3 = new Vector3(-0.5f, -0.5f, -0.5f);
Vector3 p4 = new Vector3(-0.5f, 0.5f, 0.5f);
Vector3 p5 = new Vector3(0.5f, 0.5f, 0.5f);
Vector3 p6 = new Vector3(0.5f, 0.5f, -0.5f);
Vector3 p7 = new Vector3(-0.5f, 0.5f, -0.5f);
switch (side)
{
case Cubeside.BOTTOM:
vertices = new Vector3[] { p0, p1, p2, p3 };
normals = new Vector3[] {Vector3.down, Vector3.down,
Vector3.down, Vector3.down};
uvs = new Vector2[] { uv11, uv01, uv00, uv10 };
triangles = new int[] { 3, 1, 0, 3, 2, 1 };
break;
case Cubeside.TOP:
vertices = new Vector3[] { p7, p6, p5, p4 };
normals = new Vector3[] {Vector3.up, Vector3.up,
Vector3.up, Vector3.up};
uvs = new Vector2[] { uv11, uv01, uv00, uv10 };
triangles = new int[] { 3, 1, 0, 3, 2, 1 };
break;
case Cubeside.LEFT:
vertices = new Vector3[] { p7, p4, p0, p3 };
normals = new Vector3[] {Vector3.left, Vector3.left,
Vector3.left, Vector3.left};
uvs = new Vector2[] { uv11, uv01, uv00, uv10 };
triangles = new int[] { 3, 1, 0, 3, 2, 1 };
break;
case Cubeside.RIGHT:
vertices = new Vector3[] { p5, p6, p2, p1 };
normals = new Vector3[] {Vector3.right, Vector3.right,
Vector3.right, Vector3.right};
uvs = new Vector2[] { uv11, uv01, uv00, uv10 };
triangles = new int[] { 3, 1, 0, 3, 2, 1 };
break;
case Cubeside.FRONT:
vertices = new Vector3[] { p4, p5, p1, p0 };
normals = new Vector3[] {Vector3.forward, Vector3.forward,
Vector3.forward, Vector3.forward};
uvs = new Vector2[] { uv11, uv01, uv00, uv10 };
triangles = new int[] { 3, 1, 0, 3, 2, 1 };
break;
case Cubeside.BACK:
vertices = new Vector3[] { p6, p7, p3, p2 };
normals = new Vector3[] {Vector3.back, Vector3.back,
Vector3.back, Vector3.back};
uvs = new Vector2[] { uv11, uv01, uv00, uv10 };
triangles = new int[] { 3, 1, 0, 3, 2, 1 };
break;
}
mesh.vertices = vertices;
mesh.normals = normals;
mesh.uv = uvs;
mesh.triangles = triangles;
mesh.RecalculateBounds();
GameObject quad = new GameObject("Quad");
quad.transform.parent = this.gameObject.transform;
MeshFilter meshFilter = (MeshFilter)quad.AddComponent(typeof(MeshFilter));
meshFilter.mesh = mesh;
MeshRenderer renderer = quad.AddComponent(typeof(MeshRenderer)) as MeshRenderer;
renderer.material = cubeMaterial;
}
void CombineQuads()
{
MeshFilter[] meshFilters = GetComponentsInChildren<MeshFilter>();
CombineInstance[] combine = new CombineInstance[meshFilters.Length];
int i = 0;
while (i < meshFilters.Length)
{
combine[i].mesh = meshFilters[i].sharedMesh;
combine[i].transform = meshFilters[i].transform.localToWorldMatrix;
i++;
}
MeshFilter mf = (MeshFilter)this.gameObject.AddComponent(typeof(MeshFilter));
mf.mesh = new Mesh();
mf.mesh.CombineMeshes(combine);
MeshRenderer renderer = this.gameObject.AddComponent(typeof(MeshRenderer)) as MeshRenderer;
renderer.material = cubeMaterial;
foreach (Transform quad in this.transform)
{
Destroy(quad.gameObject);
}
}
public void CreateCube(bool front, bool back, bool top, bool bottom, bool left, bool right)
{
if (front)
CreateQuad(Cubeside.FRONT);
if (back)
CreateQuad(Cubeside.BACK);
if (top)
CreateQuad(Cubeside.TOP);
if (bottom)
CreateQuad(Cubeside.BOTTOM);
if (left)
CreateQuad(Cubeside.LEFT);
if (right)
CreateQuad(Cubeside.RIGHT);
CombineQuads();
}
public IEnumerator CreateCubeE(bool front, bool back, bool top, bool bottom, bool left, bool right)
{
if (front)
CreateQuad(Cubeside.FRONT);
if (back)
CreateQuad(Cubeside.BACK);
if (top)
CreateQuad(Cubeside.TOP);
if (bottom)
CreateQuad(Cubeside.BOTTOM);
if (left)
CreateQuad(Cubeside.LEFT);
if (right)
CreateQuad(Cubeside.RIGHT);
yield return null;
CombineQuads();
}
}
Answer by Optrix · Jul 02, 2019 at 04:12 AM
You'd get a lot more performance if you just built a single Mesh rather than building hundreds of smaller Quad meshes and sticking them together with Combine. You're creating and destroying a LOT of objects that really don't need to be done that way. You've gone for simplicity of code, but it's destroying your performance.
Basically, you should add vertices, triangles and UV coordinates to lists until you either run out of voxels for a chunk, or hit around 64,000 vertices (Unity doesn't support meshes of greater than 65535 vertices). Once that happens, then create your GameObject and Mesh.
You can also add a colour to your vertices - these are useful for faking Ambient Occlusion (for example, the vertices that meet where a horizontal edge meet a vertical one should be a little bit darker).
It's never a trivial amount of work - voxels take time to build - but you should be able to shave off a lot of excess processing, object destruction and memory allocation.
Okay that really makes sense, i just did a few tests and just generating one mesh is much much faster. Though this is going to be a bit harder to implement i guess. Sometimes you can´t see the things that are in plain sight :D
Thank you :)
Answer by jamesheaton · Oct 07, 2020 at 09:44 AM
Did you ever manage to resolve this? I am also having the same problem and my code is similar - I believe we both got it from the same Udemy course :) ,Did you ever manage to resolve this?
Hey, i decided to write it again and after that i saw how bad the old code was. I am now able to generate the chunks extremly quick and have no lag when walking around and generating new chunks. The course really only taught some basics but has a few flaws that really kill performance. What you need to do is exactly what @Optrix described and it should point you into the correct direction.
Would you be able to share this code? I looked into this briefly but I don't really know where to start. I suppose the main problem is converting the vertices on each of the quads into a world position but there is quite a lot more to be done, just wondering if you could give me a little more help.
right now i do not have the code on me, i will reply to it once i do :)
Answer by KidsWithSpraycans · May 20 at 12:46 PM
Although this post is specific to voxel terrain, anyone looking into making a voxel game should try to optimize their 3D voxel models, since they can be quite inefficient.
I've created the Ultimate Voxel Optimizer as a way of helping this problem. Using classic meshing techniques with voxel models doesn't result in the most optimal models. UVO, however, uses a custom meshing algorithm to ensure that you get the best reduction out of all model optimizer programs.
Simply load in your .vox models from MagicaVoxel, select your export method and format, and click export. Some models can receive a 50% reduction in polygon count, with more features coming soon! The program is currently $3.99, and available on Windows, Mac, and Linux!
You can get more info at https://nateonus.itch.io/ultimate-voxel-optimizer