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.

I’m no Markus Persson, but…

you can start here

unity marching cubes minecraft

// 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*,cellSize) );
            for( int i=0 ; i<predefined.indices.Length ; i++ )
                indices.Add( index + predefined.indices *);
            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 );
    }
}