Optimizing boxels

Hey everyone, i’ve been working on a boxel engine (think minecraft) for a while now and the mesh generation is insanely fast, it just has trouble with the amount of vertices being rendered.



Optimizations already in place:

  • (atm it’s only using one chunk 50x50x50)
  • the 3d array is looped through and any faces which are adjacent to another block aren’t rendered.
  • vertice / lighting etc is stored inside the 3d array. (lighting is just colour data for vertices)
  • each face is made up of 4 vertices.



    Now, compared to other boxel engines I think the amount of blocks rendered is pretty sub par, can anyone think of any other optimizations I could do to make things more efficient?



    Also, my vertice data is stored as vector3f’s and put into a float buffer, does jmonkey support the use of shorts for vertice data? (each vertice is a whole number).



    I thought of a few ideas such as using the same vertices for adjacent blocks and things, but they all lead to issues with textures and my lighting system (using vertice colors).



    I’m using a texture atlas.

don’t have the back faces in the first place. “voxel” means that you create a mesh that looks like the surface of stacked boxes, not that you stack boxes.

Yes, that’s what i’m doing, I am only adding the vertices for the faces that are being rendered to the buffer, the other vertices are just being stored inside a “BlockData” class to work as a look up table so I don’t have to define “new” vector3f’s whenever I regenerate the mesh.

So the looping doesn’t happen continuously? Also why you talk about Vector3f’s? You create the mesh by modifying the vertex etc. buffer directly, right? If you don’t, thats where you can optimize.

[java]/*

  • To change this template, choose Tools | Templates
  • and open the template in the editor.

    */

    package mygame;



    import com.jme3.material.Material;

    import com.jme3.math.FastMath;

    import com.jme3.math.Vector2f;

    import com.jme3.math.Vector3f;

    import com.jme3.renderer.RenderManager;

    import com.jme3.renderer.ViewPort;

    import com.jme3.scene.Geometry;

    import com.jme3.scene.Mesh;

    import com.jme3.scene.Node;

    import com.jme3.scene.Spatial;

    import com.jme3.scene.VertexBuffer.Type;

    import com.jme3.scene.control.AbstractControl;

    import com.jme3.scene.control.Control;

    import com.jme3.util.BufferUtils;



    /**

    *
  • @author Azura

    /

    public class VoxelManager extends AbstractControl

    {



    private static final int FACE_UP = 0, FACE_LEFT = 1, FACE_FRONT = 2, FACE_BACK = 3;



    private static final float[][] COLOR_LIT = {{1,1,1,1f},{.8f,.8f,.8f,1f},{.9f,.9f,.9f,1f},{.8f,.8f,.8f,1f}};

    private static final float[][] COLOR_DIM = {{.85f,.85f,.85f,1f},{.65f,.65f,.65f,1f},{.75f,.75f,.75f,1f},{.65f,.65f,.65f,1f}};



    private float updateCounter = 0;

    private int faceCounter = 0;



    private Material matTilesheet;

    private VoxelData[][][] voxelMap;

    private Vector3f size;

    private Node parentNode;



    public VoxelManager(Material matTilesheet, VoxelData[][][] voxelMap, Vector3f size, Node parentNode)

    {

    this.matTilesheet = matTilesheet;

    this.voxelMap = voxelMap;

    this.size = size;

    this.parentNode = parentNode;



    for(int x = 1; x < size.getX()-1; x++)

    {

    for(int y = 1; y < size.getY()-1; y++)

    {

    for(int z = 1; z < size.getZ()-1; z++)

    {

    if(voxelMap[x][y][z] != null)

    {

    castLight(x,y,z);

    }

    }

    }

    }



    buildChunk();

    }



    @Override

    protected void controlUpdate(float tpf)

    {

    updateCounter += tpf;



    if(updateCounter > 0.25f)

    {

    updateCounter -= 0.25f;

    //for(int i = 0; i < 5; i++)

    //removeBlock();

    //buildChunk();

    //System.out.println("hm.");

    }

    }



    private void removeBlock()

    {

    int x = FastMath.nextRandomInt(1,(int)size.x-2);

    int y = FastMath.nextRandomInt(1,(int)size.y-2);

    int z = FastMath.nextRandomInt(1,(int)size.z-2);



    if(voxelMap[x][y][z] != null)

    {

    faceCounter -= voxelMap[x][y][z].getFaceCount();



    voxelMap[x][y][z] = null;

    if(voxelMap[x+1][y][z] != null){voxelMap[x+1][y][z].setNeedsUpdate(true);}

    if(voxelMap[x][y+1][z] != null){voxelMap[x][y+1][z].setNeedsUpdate(true);}

    if(voxelMap[x][y][z+1] != null){voxelMap[x][y][z+1].setNeedsUpdate(true);}

    if(voxelMap[x][y][z-1] != null){voxelMap[x][y][z-1].setNeedsUpdate(true);}



    castLightAround(x,y,z);

    }

    }



    private void castLightAround(int x, int y, int z)

    {

    castLight(x,y,z);

    castLight(x+1,y,z);



    castLight(x,y,z+1);

    castLight(x+1,y,z+1);



    castLight(x-1,y,z);



    castLight(x,y,z-1);

    castLight(x-1,y,z-1);

    }



    private void buildChunk()

    {

    Vector3f[] vertices = null;

    int[] indices = null;

    Vector3f[] normals = null;

    Vector2f[] texCoords = null;

    float[] verticesColor = null;



    for(int x = 1; x < size.getX()-1; x++)

    {

    for(int y = 1; y < size.getY()-1; y++)

    {

    for(int z = 1; z < size.getZ()-1; z++)

    {

    if(voxelMap[x][y][z] != null)

    {

    if(voxelMap[x][y][z].isNeedsUpdate())

    {

    checkFaces(x,y,z);

    voxelMap[x][y][z].setNeedsUpdate(false);

    }

    }

    }

    }

    }



    vertices = new Vector3f[faceCounter
    4];

    indices = new int[faceCounter6];

    normals = new Vector3f[faceCounter
    4];

    texCoords = new Vector2f[faceCounter4];

    verticesColor = new float[faceCounter
    16];



    int verticePosition = 0;

    int indicePosition = 0;

    int verticesColorPosition = 0;



    for(int x = 1; x < size.x-1; x++)

    {

    for(int y = 1; y < size.y-1; y++)

    {

    for(int z = 1; z < size.z-1; z++)

    {

    if(voxelMap[x][y][z] != null)

    {

    for(int i = 0; i < 4; i++)

    {

    if(voxelMap[x][y][z].getFace(i) == true)

    {

    int oldVertPos = verticePosition;

    Vector3f[] faceVertices = voxelMap[x][y][z].getVertices(i);

    Vector3f[] faceNormals = voxelMap[x][y][z].getNormals(i);

    Vector2f[] faceTexCoords = voxelMap[x][y][z].getTexCoords(i);

    float[] faceVerticesColor;



    if(voxelMap[x][y][z].getLit())

    {

    faceVerticesColor = COLOR_LIT;

    }

    else

    {

    faceVerticesColor = COLOR_DIM;

    }



    for(int j = 0; j < 4; j++)

    {

    vertices[verticePosition] = faceVertices[j];

    normals[verticePosition] = faceNormals[j];

    texCoords[verticePosition] = faceTexCoords[j];

    verticePosition++;

    }



    for(int j = 0; j < 4; j++)

    {

    for(int k = 0; k < 4; k++)

    {

    verticesColor[verticesColorPosition] = faceVerticesColor[k];

    verticesColorPosition++;

    }

    }



    indices[indicePosition] = oldVertPos + 2;

    indicePosition++;

    indices[indicePosition] = oldVertPos + 0;

    indicePosition++;

    indices[indicePosition] = oldVertPos + 1;

    indicePosition++;

    indices[indicePosition] = oldVertPos + 1;

    indicePosition++;

    indices[indicePosition] = oldVertPos + 3;

    indicePosition++;

    indices[indicePosition] = oldVertPos + 2;

    indicePosition++;

    }

    }

    }

    }

    }

    }



    Mesh areaMesh = new Mesh();

    areaMesh = new Mesh();



    areaMesh.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(vertices));

    areaMesh.setBuffer(Type.Normal, 3, BufferUtils.createFloatBuffer(normals));

    areaMesh.setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(texCoords));

    areaMesh.setBuffer(Type.Index, 1, BufferUtils.createIntBuffer(indices));

    areaMesh.setBuffer(Type.Color, 4, verticesColor);



    areaMesh.updateBound();



    parentNode.detachChildNamed("islandMesh");

    // Creating a geometry, and apply a single color material to it

    Geometry levelGeom = new Geometry("islandMesh", areaMesh);

    levelGeom.setMaterial(matTilesheet);



    parentNode.attachChild(levelGeom);

    }



    private void checkFaces(int x, int y, int z)

    {

    int counter = 0;

    boolean[] faces = {false, false, false, false};



    // up face

    if(voxelMap[x][y-1][z] == null)

    {

    faces[FACE_UP] = true;

    counter++;

    faceCounter++;

    }



    // left face

    if(voxelMap[x-1][y][z] == null)

    {

    faces[FACE_LEFT] = true;

    counter++;

    faceCounter++;

    }



    // back face

    if(voxelMap[x][y][z+1] == null)

    {

    faces[FACE_BACK] = true;

    counter++;

    faceCounter++;

    }



    // front face

    if(voxelMap[x][y][z-1] == null)

    {

    faces[FACE_FRONT] = true;

    counter++;

    faceCounter++;

    }



    voxelMap[x][y][z].setFaces(faces);

    voxelMap[x][y][z].setFaceCount(counter);

    }



    private void castLight(int x, int y, int z)

    {

    int lowest = x;

    if(lowest > y)lowest = y;

    if(lowest > z)lowest = z;



    int iX=x-lowest, iY=y-lowest, iZ=z-lowest;

    boolean light = true;



    while(iX < (size.getX()-1) && iY < (size.getY()-1) && iZ < (size.getZ()-1))

    {

    boolean[] lit = {false,false,false,false};







    if(voxelMap[iX][iY][iZ] != null)

    {

    if(light)

    {

    if(voxelMap[iX-1][iY-1][iZ] == null && voxelMap[iX][iY-1][iZ-1] == null)

    {

    lit[0] = true;

    }



    if(voxelMap[iX-1][iY][iZ-1] == null)

    {

    if(voxelMap[iX-1][iY-1][iZ] == null)

    {

    lit[1] = true;

    }

    if(voxelMap[iX][iY-1][iZ-1] == null)

    {

    lit[2] = true;

    }

    }



    voxelMap[iX][iY][iZ].setLight(true);

    light = false;

    }

    else

    {

    if(voxelMap[iX][iY][iZ].getLight() == false)

    {

    break;

    }



    lit[0] = false;

    lit[1] = false;

    lit[2] = false;

    voxelMap[iX][iY][iZ].setLight(false);

    }

    voxelMap[iX][iY][iZ].setLit(lit);

    }



    if(light)

    {

    if(voxelMap[iX+1][iY+1][iZ+1] == null)

    {

    if(!(voxelMap[iX+1][iY][iZ+1] == null && voxelMap[iX+1][iY][iZ] == null &&

    voxelMap[iX][iY][iZ+1] == null))

    {

    light = false;

    }

    }

    }



    iX++;

    iY++;

    iZ++;

    }

    }



    @Override

    protected void controlRender(RenderManager rm, ViewPort vp)

    {

    //throw new UnsupportedOperationException("Not supported yet.");

    }



    public Control cloneForSpatial(Spatial spatial)

    {

    throw new UnsupportedOperationException("Not supported yet.");

    }

    }[/java]

Hm, you have big arrays that mostly will consist of empty space… And then you loop through them. I guess you should keep a list of only the boxes that actually exist. More like a node/octree system or something like that instead of these kinds of arrays.

1 Like

Something fishy must be going on. What exactly do you mean by “has trouble with the amount of vertices being rendered”?



Does it render but slowly? Not render at all? Render only part of your chunk?

I’ll look into implementing an octree or something of the sorts :slight_smile:

It renders, but on any machine i’ve tested it on that isn’t mine, it has trouble with the mesh so the fps lowers insanely. (I have an i7 with 16gb of ram etc etc so I can’t use my computer for testing purposes).



On my computer a fragmented version of a 50 x 50 x 50 chunk runs at about 800 fps, on my friends computers it’s around 200. Is this a normal speed?

What do you mean by “a fragmented version of a 50 x 50 x 50 chunk”?



I don’t think an octree will buy you anything in this case.



Note: unless you are recalculating your mesh every frame (really bad) then your i7 and 16gb of ram say nothing about how this will run since it is entirely dependent on the graphics card.