- Home /
Question by
Joseph_Coder · Jun 28, 2021 at 07:59 AM ·
performanceoptimizationprocedural meshprocedural generationprocedural-terrain
How to create voxel-based procedural terrain
I'm trying to make a game with a procedural generation terrain in 3d, but I don't know how to start making it, I would like to make something like this image to start:
And I did some research and tried to make at least the vertex array, here is some of the code:
// The code below is just an exemple of how I organized it
Vector3[][] vertices = new Vector3[6][];
for (z = 0; z <= zSize; z++)
{
for (int x = 0; x <= xSize; x++)
{
for (int y = 0; y <= ySize; y++)
{
var blockPos = new Vector3(x, y, z);
vertices = new Vector3[][]
{
new Vector3[] { //Front
new Vector3(1, 0, 1) + blockPos,
new Vector3(1, 1, 1) + blockPos,
new Vector3(0, 1, 1) + blockPos,
new Vector3(0, 0, 1) + blockPos
},
new Vector3[] { //Left
new Vector3(0, 0, 1) + blockPos,
new Vector3(0, 1, 1) + blockPos,
new Vector3(0, 1, 0) + blockPos,
new Vector3(0, 0, 0) + blockPos
},
new Vector3[] { //Back
new Vector3(0, 0, 0) + blockPos,
new Vector3(0, 1, 0) + blockPos,
new Vector3(1, 1, 0) + blockPos,
new Vector3(1, 0, 0) + blockPos
},
new Vector3[] { //Right
new Vector3(1, 0, 0) + blockPos,
new Vector3(1, 1, 0) + blockPos,
new Vector3(1, 1, 1) + blockPos,
new Vector3(1, 0, 1) + blockPos
},
new Vector3[] { //Top
new Vector3(0, 1, 0) + blockPos,
new Vector3(0, 1, 1) + blockPos,
new Vector3(1, 1, 1) + blockPos,
new Vector3(1, 1, 0) + blockPos
},
new Vector3[] { //Bottom
new Vector3(0, 0, 0) + blockPos,
new Vector3(1, 0, 0) + blockPos,
new Vector3(1, 0, 1) + blockPos,
new Vector3(0, 0, 1) + blockPos
},
};
} // 3
} // 2
} // 1
I was looking for an optimized way to create it, but I really don't know how I would set the triangles of the mesh and get a result like the image above.
example.png
(449.9 kB)
Comment
Best Answer
Answer by andrew-lukasik · Jun 28, 2021 at 09:51 PM
I'm no Markus Persson, but...
you can start here
// src: https://gist.github.com/andrew-raphael-lukasik/a18fbc436fb1e19e19e8d9557e0dfe69
using System.Collections.Generic;
using UnityEngine;
[RequireComponent( typeof(MeshFilter) , typeof(MeshRenderer) )]
public class ImNoMarkusPerssonBut : MonoBehaviour
{
[SerializeField] Vector3 _worldSize = new Vector3( 30 , 5 , 30 );
[SerializeField] Vector3Int _numCells = new Vector3Int( 100 , 20 , 100 );
[SerializeField] Vector3Int _cellOrigin = new Vector3Int( 0 , 0 , 0 );
[SerializeField] float _perlinNoiseScale = 0.07f;
[SerializeField] float _surfaceHeight = 8.5f;
Mesh _mesh;
#if UNITY_EDITOR
void OnValidate () { RecreateMesh(); }
#endif
void OnEnable () { RecreateMesh(); }
void OnDisable () { Dispose(_mesh); }
void RecreateMesh ()
{
if( _mesh==null ) Dispose( _mesh );
_mesh = new Mesh();
_mesh.hideFlags = HideFlags.DontSave;
GetComponent<MeshFilter>().sharedMesh = _mesh;
GenerateMesh( _mesh , numCells:_numCells , cellOrigin:_cellOrigin , worldSize:_worldSize , surfaceHeight:_surfaceHeight , noiseScale:_perlinNoiseScale );
}
/// <summary>
/// Decides if a block with given XYZ coordinate will be solid or just empty space.
/// </summary>
static int SampleProceduralField ( Vector3 point , float surfaceHeight , float noiseScale )
{
float value = Mathf.PerlinNoise(
point.x * noiseScale ,
point.z * noiseScale
);
float h = point.y / surfaceHeight;
return value>h ? 1 : 0;
}
/// <summary>
/// Generates mesh not from data but by asking SampleProceduralField.
/// </summary>
static void GenerateMesh ( Mesh mesh , Vector3Int numCells , Vector3Int cellOrigin , Vector3 worldSize , float surfaceHeight , float noiseScale )
{
List<Vector3> vertices = new List<Vector3>(32);
List<int> indices = new List<int>(32);
int index = 0;
Vector3 cellSize = new Vector3( worldSize.x/numCells.x , worldSize.y/numCells.y , worldSize.z/numCells.z );
// iterates every block coordinate:
for( int y=0 ; y<numCells.y ; y++ )
for( int z=0 ; z<numCells.z ; z++ )
for( int x=0 ; x<numCells.x ; x++ )
{
// samples a position that will tell us if this block is solid or not
int cell = SampleProceduralField( new Vector3Int(x,y,z)+cellOrigin , surfaceHeight:surfaceHeight , noiseScale );
if( cell==0 ) continue;// ignore empty cells
// samples 6 neighbouring cells (blocks) so we know if those are solid or not...
int right = SampleProceduralField( new Vector3Int(x+1,y,z) + cellOrigin , surfaceHeight:surfaceHeight , noiseScale:noiseScale );
int left = SampleProceduralField( new Vector3Int(x-1,y,z) + cellOrigin , surfaceHeight:surfaceHeight , noiseScale:noiseScale );
int above = SampleProceduralField( new Vector3Int(x,y+1,z) + cellOrigin , surfaceHeight:surfaceHeight , noiseScale:noiseScale );
int under = SampleProceduralField( new Vector3Int(x,y-1,z) + cellOrigin , surfaceHeight:surfaceHeight , noiseScale:noiseScale );
int front = SampleProceduralField( new Vector3Int(x,y,z+1) + cellOrigin , surfaceHeight:surfaceHeight , noiseScale:noiseScale );
int back = SampleProceduralField( new Vector3Int(x,y,z-1) + cellOrigin , surfaceHeight:surfaceHeight , noiseScale:noiseScale );
// .. and then encodes that info as a simple bitmask...
int neighbours = right<<5 | left<<4 | above<<3 | under<<2 | front<<1 | back<<0;
// ...which is used to look up a human-defined mesh data that helps us create this mesh:
var predefined = LOOKUP[neighbours];
//
Vector3 cellCenter = Vector3.Scale(new Vector3(x,y,z),cellSize) + cellSize*0.5f;
for( int i=0 ; i<predefined.vertices.Length ; i++ )
vertices.Add( cellCenter + Vector3.Scale(predefined.vertices[i],cellSize) );
for( int i=0 ; i<predefined.indices.Length ; i++ )
indices.Add( index + predefined.indices[i] );
index += predefined.vertices.Length;
}
// updates mesh:
mesh.SetVertices( vertices );
mesh.SetIndices( indices , MeshTopology.Triangles , submesh:0 , calculateBounds:true );
mesh.RecalculateNormals();
mesh.UploadMeshData( markNoLongerReadable:true );
}
const float P = 0.5f, N = -0.5f;// just creates a shorthand for 0.5f and -0.5f values
/// shorthand for 4 vertices that constitute most blocks
static readonly Vector3[] k_VERTICES_4 = new Vector3[8]{ new Vector3(N,P,N) , new Vector3(N,P,P) , new Vector3(P,P,P) , new Vector3(P,P,N) , new Vector3(N,N,N) , new Vector3(N,N,P) , new Vector3(P,N,P) , new Vector3(P,N,N) };
///
/// This is a lookup table that allows you to define vertex/triangle index data for each block-neighbourhood configuration (encoded as a bitmask here)
///
/// neighbour bitmask: 1 means solid, 0 means air
/// 5 , 4 , 3 , 2 , 1 , 0 (bit index, starting from the right)
/// x+ , x- , y+ , y- , z+ , z- (direction)
///
/// For example:
/// 0b_001100 - is a bitmask of a block that has 2 neighbouring blocks, one above and one below.
///
static Dictionary<int,(Vector3[] vertices,int[] indices)> LOOKUP = new Dictionary<int,(Vector3[],int[])>
{
{ 0b_000000 , (
k_VERTICES_4 ,
new int[6*6]{ 7,3,2,7,2,6, 5,1,0,5,0,4, 0,1,2,0,2,3, 5,4,7,5,7,6, 6,2,1,6,1,5, 4,0,3,4,3,7 }
) } ,
{ 0b_100000 , (
k_VERTICES_4 ,
new int[5*6]{ 5,1,0,5,0,4, 0,1,2,0,2,3, 5,4,7,5,7,6, 6,2,1,6,1,5, 4,0,3,4,3,7 }
) } ,
{ 0b_010000 , (
k_VERTICES_4 ,
new int[5*6]{ 7,3,2,7,2,6, 0,1,2,0,2,3, 5,4,7,5,7,6, 6,2,1,6,1,5, 4,0,3,4,3,7 }
) } ,
{ 0b_110000 , (
k_VERTICES_4 ,
new int[4*6]{ 0,1,2,0,2,3, 5,4,7,5,7,6, 6,2,1,6,1,5, 4,0,3,4,3,7 }
) } ,
{ 0b_001000 , (
k_VERTICES_4 ,
new int[5*6]{ 7,3,2,7,2,6, 5,1,0,5,0,4, 5,4,7,5,7,6, 6,2,1,6,1,5, 4,0,3,4,3,7 }
) } ,
{ 0b_101000 , (
k_VERTICES_4 ,
new int[4*6]{ 5,1,0,5,0,4, 5,4,7,5,7,6, 6,2,1,6,1,5, 4,0,3,4,3,7 }
) } ,
{ 0b_011000 , (
k_VERTICES_4 ,
new int[4*6]{ 7,3,2,7,2,6, 5,4,7,5,7,6, 6,2,1,6,1,5, 4,0,3,4,3,7 }
) } ,
{ 0b_111000 , (
k_VERTICES_4 ,
new int[3*6]{ 5,4,7,5,7,6, 6,2,1,6,1,5, 4,0,3,4,3,7 }
) } ,
{ 0b_000100 , (
k_VERTICES_4 ,
new int[5*6]{ 7,3,2,7,2,6, 5,1,0,5,0,4, 0,1,2,0,2,3, 6,2,1,6,1,5, 4,0,3,4,3,7 }
) } ,
{ 0b_100100 , (
k_VERTICES_4 ,
new int[4*6]{ 5,1,0,5,0,4, 0,1,2,0,2,3, 6,2,1,6,1,5, 4,0,3,4,3,7 }
) } ,
{ 0b_010100 , (
k_VERTICES_4 ,
new int[4*6]{ 7,3,2,7,2,6, 0,1,2,0,2,3, 6,2,1,6,1,5, 4,0,3,4,3,7 }
) } ,
{ 0b_110100 , (
k_VERTICES_4 ,
new int[3*6]{ 0,1,2,0,2,3, 6,2,1,6,1,5, 4,0,3,4,3,7 }
) } ,
{ 0b_001100 , (
k_VERTICES_4 ,
new int[4*6]{ 7,3,2,7,2,6, 5,1,0,5,0,4, 6,2,1,6,1,5, 4,0,3,4,3,7 }
) } ,
{ 0b_101100 , (
k_VERTICES_4 ,
new int[3*6]{ 5,1,0,5,0,4, 6,2,1,6,1,5, 4,0,3,4,3,7 }
) } ,
{ 0b_011100 , (
k_VERTICES_4 ,
new int[3*6]{ 7,3,2,7,2,6, 6,2,1,6,1,5, 4,0,3,4,3,7 }
) } ,
{ 0b_111100 , (
k_VERTICES_4 ,
new int[2*6]{ 6,2,1,6,1,5, 4,0,3,4,3,7 }
) } ,
{ 0b_000010 , (
k_VERTICES_4 ,
new int[5*6]{ 7,3,2,7,2,6, 5,1,0,5,0,4, 0,1,2,0,2,3, 5,4,7,5,7,6, 4,0,3,4,3,7 }
) } ,
{ 0b_100010 , (
k_VERTICES_4 ,
new int[4*6]{ 5,1,0,5,0,4, 0,1,2,0,2,3, 5,4,7,5,7,6, 4,0,3,4,3,7 }
) } ,
{ 0b_010010 , (
k_VERTICES_4 ,
new int[4*6]{ 7,3,2,7,2,6, 0,1,2,0,2,3, 5,4,7,5,7,6, 4,0,3,4,3,7 }
) } ,
{ 0b_110010 , (
k_VERTICES_4 ,
new int[3*6]{ 0,1,2,0,2,3, 5,4,7,5,7,6, 4,0,3,4,3,7 }
) } ,
{ 0b_001010 , (
k_VERTICES_4 ,
new int[4*6]{ 7,3,2,7,2,6, 5,1,0,5,0,4, 5,4,7,5,7,6, 4,0,3,4,3,7 }
) } ,
{ 0b_101010 , (
k_VERTICES_4 ,
new int[3*6]{ 5,1,0,5,0,4, 5,4,7,5,7,6, 4,0,3,4,3,7 }
) } ,
{ 0b_011010 , (
k_VERTICES_4 ,
new int[3*6]{ 7,3,2,7,2,6, 5,4,7,5,7,6, 4,0,3,4,3,7 }
) } ,
{ 0b_111010 , (
k_VERTICES_4 ,
new int[2*6]{ 5,4,7,5,7,6, 4,0,3,4,3,7 }
) } ,
{ 0b_000110 , (
k_VERTICES_4 ,
new int[4*6]{ 7,3,2,7,2,6, 5,1,0,5,0,4, 0,1,2,0,2,3, 4,0,3,4,3,7 }
) } ,
{ 0b_100110 , (
k_VERTICES_4 ,
new int[3*6]{ 5,1,0,5,0,4, 0,1,2,0,2,3, 4,0,3,4,3,7 }
) } ,
{ 0b_010110 , (
k_VERTICES_4 ,
new int[3*6]{ 7,3,2,7,2,6, 0,1,2,0,2,3, 4,0,3,4,3,7 }
) } ,
{ 0b_110110 , (
k_VERTICES_4 ,
new int[2*6]{ 0,1,2,0,2,3, 4,0,3,4,3,7 }
) } ,
{ 0b_001110 , (
k_VERTICES_4 ,
new int[3*6]{ 7,3,2,7,2,6, 5,1,0,5,0,4, 4,0,3,4,3,7 }
) } ,
{ 0b_101110 , (
k_VERTICES_4 ,
new int[2*6]{ 5,1,0,5,0,4, 4,0,3,4,3,7 }
) } ,
{ 0b_011110 , (
k_VERTICES_4 ,
new int[2*6]{ 7,3,2,7,2,6, 4,0,3,4,3,7 }
) } ,
{ 0b_111110 , (
new Vector3[]{ new Vector3(N,N,N) , new Vector3(N,P,N) , new Vector3(P,P,N) , new Vector3(P,N,N) } ,
new int[6]{ 0,1,2,0,2,3 }
) } ,
{ 0b_000001 , (
k_VERTICES_4 ,
new int[5*6]{ 7,3,2,7,2,6, 5,1,0,5,0,4, 0,1,2,0,2,3, 5,4,7,5,7,6, 6,2,1,6,1,5, }
) } ,
{ 0b_100001 , (
k_VERTICES_4 ,
new int[4*6]{ 5,1,0,5,0,4, 0,1,2,0,2,3, 5,4,7,5,7,6, 6,2,1,6,1,5, }
) } ,
{ 0b_010001 , (
k_VERTICES_4 ,
new int[4*6]{ 7,3,2,7,2,6, 0,1,2,0,2,3, 5,4,7,5,7,6, 6,2,1,6,1,5, }
) } ,
{ 0b_110001 , (
k_VERTICES_4 ,
new int[3*6]{ 0,1,2,0,2,3, 5,4,7,5,7,6, 6,2,1,6,1,5, }
) } ,
{ 0b_001001 , (
k_VERTICES_4 ,
new int[4*6]{ 7,3,2,7,2,6, 5,1,0,5,0,4, 5,4,7,5,7,6, 6,2,1,6,1,5, }
) } ,
{ 0b_101001 , (
k_VERTICES_4 ,
new int[3*6]{ 5,1,0,5,0,4, 5,4,7,5,7,6, 6,2,1,6,1,5, }
) } ,
{ 0b_011001 , (
k_VERTICES_4 ,
new int[3*6]{ 7,3,2,7,2,6, 5,4,7,5,7,6, 6,2,1,6,1,5, }
) } ,
{ 0b_111001 , (
k_VERTICES_4 ,
new int[2*6]{ 5,4,7,5,7,6, 6,2,1,6,1,5, }
) } ,
{ 0b_000101 , (
k_VERTICES_4 ,
new int[4*6]{ 7,3,2,7,2,6, 5,1,0,5,0,4, 0,1,2,0,2,3, 6,2,1,6,1,5, }
) } ,
{ 0b_100101 , (
k_VERTICES_4 ,
new int[3*6]{ 5,1,0,5,0,4, 0,1,2,0,2,3, 6,2,1,6,1,5, }
) } ,
{ 0b_010101 , (
k_VERTICES_4 ,
new int[3*6]{ 7,3,2,7,2,6, 0,1,2,0,2,3, 6,2,1,6,1,5, }
) } ,
{ 0b_110101 , (
k_VERTICES_4 ,
new int[2*6]{ 0,1,2,0,2,3, 6,2,1,6,1,5, }
) } ,
{ 0b_001101 , (
k_VERTICES_4 ,
new int[3*6]{ 7,3,2,7,2,6, 5,1,0,5,0,4, 6,2,1,6,1,5, }
) } ,
{ 0b_101101 , (
k_VERTICES_4 ,
new int[2*6]{ 5,1,0,5,0,4, 6,2,1,6,1,5, }
) } ,
{ 0b_011101 , (
k_VERTICES_4 ,
new int[2*6]{ 7,3,2,7,2,6, 6,2,1,6,1,5, }
) } ,
{ 0b_111101 , (
new Vector3[4]{ new Vector3(P,N,P) , new Vector3(P,P,P) , new Vector3(N,P,P) , new Vector3(N,N,P) } ,
new int[6]{ 0,1,2,0,2,3, }
) } ,
{ 0b_000011 , (
k_VERTICES_4 ,
new int[4*6]{ 7,3,2,7,2,6, 5,1,0,5,0,4, 0,1,2,0,2,3, 5,4,7,5,7,6, }
) } ,
{ 0b_100011 , (
k_VERTICES_4 ,
new int[3*6]{ 5,1,0,5,0,4, 0,1,2,0,2,3, 5,4,7,5,7,6, }
) } ,
{ 0b_010011 , (
k_VERTICES_4 ,
new int[3*6]{ 7,3,2,7,2,6, 0,1,2,0,2,3, 5,4,7,5,7,6, }
) } ,
{ 0b_110011 , (
k_VERTICES_4 ,
new int[2*6]{ 0,1,2,0,2,3, 5,4,7,5,7,6, }
) } ,
{ 0b_001011 , (
k_VERTICES_4 ,
new int[3*6]{ 7,3,2,7,2,6, 5,1,0,5,0,4, 5,4,7,5,7,6, }
) } ,
{ 0b_101011 , (
k_VERTICES_4 ,
new int[2*6]{ 5,1,0,5,0,4, 5,4,7,5,7,6, }
) } ,
{ 0b_011011 , (
k_VERTICES_4 ,
new int[2*6]{ 7,3,2,7,2,6, 5,4,7,5,7,6, }
) } ,
{ 0b_111011 , (
new Vector3[]{ new Vector3(N,N,P) , new Vector3(N,N,N) , new Vector3(P,N,N) , new Vector3(P,N,P) } ,
new int[6]{ 0,1,2,0,2,3 }
) } ,
{ 0b_000111 , (
k_VERTICES_4 ,
new int[3*6]{ 7,3,2,7,2,6, 5,1,0,5,0,4, 0,1,2,0,2,3, }
) } ,
{ 0b_100111 , (
k_VERTICES_4 ,
new int[2*6]{ 5,1,0,5,0,4, 0,1,2,0,2,3, }
) } ,
{ 0b_010111 , (
k_VERTICES_4 ,
new int[2*6]{ 7,3,2,7,2,6, 0,1,2,0,2,3, }
) } ,
{ 0b_110111 , (
new Vector3[4]{ new Vector3(N,P,N) , new Vector3(N,P,P) , new Vector3(P,P,P) , new Vector3(P,P,N) } ,
new int[6]{ 0,1,2,0,2,3, }
) } ,
{ 0b_001111 , (
k_VERTICES_4 ,
new int[2*6]{ 7,3,2,7,2,6, 5,1,0,5,0,4, }
) } ,
{ 0b_101111 , (
new Vector3[]{ new Vector3(N,N,P) , new Vector3(N,P,P) , new Vector3(N,P,N) , new Vector3(N,N,N) } ,
new int[6]{ 0,1,2,0,2,3 }
) } ,
{ 0b_011111 , (
new Vector3[]{ new Vector3(P,N,N) , new Vector3(P,P,N) , new Vector3(P,P,P) , new Vector3(P,N,P) } ,
new int[6]{ 0,1,2,0,2,3 }
) } ,
{ 0b_111111 , (
new Vector3[0]{} ,
new int[0]{}
) } ,
};
void Dispose ( Object obj )
{
if( Application.isPlaying ) Object.Destroy( obj );
else Object.DestroyImmediate( obj );
}
}
Wow, this is a really good terrain, it was exactly what I needed, this code seems a bit tricky to understand, but thanks!