Hello,
I am currently working on a game that uses bloxels (similar to Minecraft) as a core component. This isn’t to be a Minecraft clone but rather to facilitate an editable world that doesn’t take too much effort to understand.
I have written a custom Mesh class that loops through the chunks and then checks the neighboring blocks (including in neighboring chunks) and then adds triangles, verticies, etc. as needed. It already skips over any unused faces.
The issue is that the method to generate the mesh often takes > 1s which is unacceptable when the player edit’s the world. When I load an old version of Minecraft the world loads almost instantly (at the maximum view distance) which leads me to believe that I am doing something inefficiently.
EDIT: Some things I have already tried:
- Decreasing the size of the chunk (This mesh is already called on an alternate thread so this did help, but it made my world have too many objects for a decent FPS)
- Remove calls to external chunks (having any faces that face an external chunk simply be absent from the mesh – it looked ugly but I figured it was fine to see if external chunks were the issue – they wearn’t)
- Switch the Chunk object to use an AtomicIntegerArray to hold block IDs rather than synchronized access to a 3D short array (Block IDs are shorts) (this caused significant performance loss) (chunks must be accessible from multiple threads or performance is entirely unacceptable)
Here is my ChunkMesh:
package src.john01dav.<redacted>.client.game.world;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer;
import com.jme3.util.BufferUtils;
import src.john01dav.<redacted>.client.game.world.textures.AtlasCoordinates;
import src.john01dav.<redacted>.client.game.world.textures.TextureProvider;
import src.john01dav.<redacted>.common.blocks.BlockSide;
import src.john01dav.<redacted>.common.world.Chunk;
import src.john01dav.<redacted>.common.world.World;
import java.util.Arrays;
public class ChunkMesh extends Mesh{
private static final Vector3f POSITIVE_X = new Vector3f(1, 0, 0);
private static final Vector3f NEGATIVE_X = new Vector3f(-1, 0, 0);
private static final Vector3f POSITIVE_Y = new Vector3f(0, 1, 0);
private static final Vector3f NEGATIVE_Y = new Vector3f(0, -1, 0);
private static final Vector3f POSITIVE_Z = new Vector3f(0, 0, 1);
private static final Vector3f NEGATIVE_Z = new Vector3f(0, 0, -1);
private final Chunk chunk;
private final WorldAppState worldAppState;
private Vector3f[] verticies;
private Vector3f[] normals;
private Vector2f[] textureCoordinates;
private int[] indecies;
private int currentVertex;
private int currentIndex;
public ChunkMesh(Chunk chunk, WorldAppState worldAppState){
this.chunk = chunk;
this.worldAppState = worldAppState;
}
public void buildMeshData(){
long start = System.currentTimeMillis();
int cx, cy, cz;
verticies = new Vector3f[1024];
normals = new Vector3f[1024];
textureCoordinates = new Vector2f[1024];
indecies = new int[4096];
currentVertex = 0;
currentIndex = 0;
synchronized(chunk.getModificationLock()){
for(cx=0;cx<Chunk.SIZE;cx++){
for(cy=0;cy<World.HEIGHT;cy++){
for(cz=0;cz<Chunk.SIZE;cz++){
buildBlockData(cx, cy, cz);
}
}
}
}
verticies = Arrays.copyOf(verticies, currentVertex);
normals = Arrays.copyOf(normals, currentVertex);
textureCoordinates = Arrays.copyOf(textureCoordinates, currentVertex);
indecies = Arrays.copyOf(indecies, currentIndex);
setBuffer(VertexBuffer.Type.Position, 3, BufferUtils.createFloatBuffer(verticies));
setBuffer(VertexBuffer.Type.Normal, 3, BufferUtils.createFloatBuffer(normals));
setBuffer(VertexBuffer.Type.TexCoord, 2, BufferUtils.createFloatBuffer(textureCoordinates));
setBuffer(VertexBuffer.Type.Index, 3, BufferUtils.createIntBuffer(indecies));
int vLength = verticies.length;
int iLength = indecies.length;
//allow these arrays to be garbage collected
verticies = null;
normals = null;
textureCoordinates = null;
indecies = null;
updateBound();
System.out.println("Mesh generation took: " + (System.currentTimeMillis() - start) + "ms " + vLength + " " + iLength); //this is how I am measuring the performance of the mesh generation
}
private void buildBlockData(int cx, int cy, int cz){
if(isBlockSolid(cx, cy, cz)){
verticies = minSize(verticies, 24, currentVertex);
if(normals.length != verticies.length) normals = Arrays.copyOf(normals, verticies.length);
if(textureCoordinates.length != verticies.length) textureCoordinates = Arrays.copyOf(textureCoordinates, verticies.length);
indecies = minSize(indecies, 36, currentIndex);
Vector3f v1 = new Vector3f(cx, cy + 1, cz);
Vector3f v2 = new Vector3f(cx, cy, cz);
Vector3f v3 = new Vector3f(cx + 1, cy, cz);
Vector3f v4 = new Vector3f(cx + 1, cy + 1, cz);
Vector3f v5 = new Vector3f(cx, cy + 1, cz + 1);
Vector3f v6 = new Vector3f(cx, cy, cz + 1);
Vector3f v7 = new Vector3f(cx + 1, cy, cz + 1);
Vector3f v8 = new Vector3f(cx + 1, cy + 1, cz + 1);
TextureProvider textureProvider = worldAppState.getTextureProvider(chunk.getBlockWithLocalCoordiantes(cx, cy, cz));
if(!isBlockSolid(cx + 1, cy, cz)){
buildFace(v4, v8, v3, v7, POSITIVE_X, textureProvider.getAtlasCoordinates(BlockSide.POSITIVE_X));
}
if(!isBlockSolid(cx - 1, cy, cz)){
buildFace(v5, v1, v6, v2, NEGATIVE_X, textureProvider.getAtlasCoordinates(BlockSide.NEGATIVE_X));
}
if(!isBlockSolid(cx, cy + 1, cz)){
buildFace(v5, v8, v1, v4, POSITIVE_Y, textureProvider.getAtlasCoordinates(BlockSide.POSITIVE_Y));
}
if(!isBlockSolid(cx, cy - 1, cz)){
buildFace(v2, v3, v6, v7, NEGATIVE_Y, textureProvider.getAtlasCoordinates(BlockSide.NEGATIVE_Y));
}
if(!isBlockSolid(cx, cy, cz + 1)){
buildFace(v8, v5, v7, v6, POSITIVE_Z, textureProvider.getAtlasCoordinates(BlockSide.POSITIVE_Z));
}
if(!isBlockSolid(cx, cy, cz - 1)){
buildFace(v1, v4, v2, v3, NEGATIVE_Z, textureProvider.getAtlasCoordinates(BlockSide.NEGATIVE_Z));
}
}
}
private boolean isBlockSolid(int x, int y, int z){
try{
return chunk.getBlockWithLocalCoordiantes(x, y, z).isSolid();
}catch(ArrayIndexOutOfBoundsException e){
if(y < 0){
return true;
}else if(y >= World.HEIGHT){
return false;
}
int gx = chunk.getCoordinates().getX() * Chunk.SIZE + x;
int gz = chunk.getCoordinates().getZ() * Chunk.SIZE + z;
return chunk.getWorld().getBlockAt(gx, y, gz).isSolid();
}
}
private void buildFace(Vector3f a, Vector3f b, Vector3f c, Vector3f d, Vector3f normal, AtlasCoordinates atlasCoordinates){
int aNum = currentVertex++;
int bNum = currentVertex++;
int cNum = currentVertex++;
int dNum = currentVertex++;
verticies[aNum] = a;
verticies[bNum] = b;
verticies[cNum] = c;
verticies[dNum] = d;
normals[aNum] = normal;
normals[bNum] = normal;
normals[cNum] = normal;
normals[dNum] = normal;
textureCoordinates[aNum] = new Vector2f(atlasCoordinates.getX(), atlasCoordinates.getY());
textureCoordinates[bNum] = new Vector2f(atlasCoordinates.getX() + AtlasCoordinates.TEXTURE_SIZE, atlasCoordinates.getY());
textureCoordinates[cNum] = new Vector2f(atlasCoordinates.getX(), atlasCoordinates.getY() + AtlasCoordinates.TEXTURE_SIZE);
textureCoordinates[dNum] = new Vector2f(atlasCoordinates.getX() + AtlasCoordinates.TEXTURE_SIZE, atlasCoordinates.getY() + AtlasCoordinates.TEXTURE_SIZE);
indecies[currentIndex++] = aNum;
indecies[currentIndex++] = bNum;
indecies[currentIndex++] = cNum;
indecies[currentIndex++] = cNum;
indecies[currentIndex++] = bNum;
indecies[currentIndex++] = dNum;
}
private <T> T[] minSize(T[] array, int neededItems, int current){
int minLength = current + neededItems;
if(array.length >= minLength){
return array;
}else{
int newLength = current;
while(newLength < minLength){
newLength*=2;
}
return Arrays.copyOf(array, newLength);
}
}
private int[] minSize(int[] array, int neededItems, int current){
int minLength = current + neededItems;
if(array.length >= minLength){
return array;
}else{
int newLength = current;
while(newLength < minLength){
newLength*=2;
}
return Arrays.copyOf(array, newLength);
}
}
public Chunk getChunk(){
return chunk;
}
}