This is probably because your “voxels” are touching. The easiest way to do it like this (which is kinda like how terraria does it) is by creating a 16x16x16 mesh and using some kind of density field or 3D noise function to determine whether a block exists or not, and modifying the mesh buffers (which will be of constant size) based on that.
Then, using that data, build a seperate collision mesh. The code below has some notes, but the code should hopefully speak for itself. The main things you want to look at are the bulidMesh()
and buildCollisionMesh
methods - but I’ve included the whole class for the sake of it.
But a final note I should really say - This is not the best way to make a “minecraft style” game. This is for a terraria-style game - and you will need to implement it in three dimensions - however I posted it because it explains how to solve your issue in particular - by building a mesh as a whole - and not individually; building a collision mesh seperately; and modifying a mesh on-the-fly instead of re-creating it again and again.
package com.jayfella.survival.chunk;
import com.jayfella.survival.Main;
import com.jayfella.survival.material.Material;
import com.jayfella.survival.material.MaterialFace;
import com.jayfella.survival.material.TexturedMaterial;
import com.jayfella.survival.volume.ArrayDensityVolume;
import com.jme3.bullet.collision.shapes.MeshCollisionShape;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.queue.RenderQueue.Bucket;
import com.jme3.scene.Geometry;
import com.jme3.scene.Mesh;
import com.jme3.scene.VertexBuffer;
import com.jme3.scene.VertexBuffer.Type;
import com.jme3.util.BufferUtils;
import java.util.ArrayList;
import java.util.List;
/**
* Represents a section of a chunk
*/
public class ChunkSection {
public static int SIZE_XY = 16;
private final WorldChunk chunk;
private final int y;
private ArrayDensityVolume densityVolume;
private Geometry geometry;
private RigidBodyControl rigidBodyControl;
private TexturedMaterial[][] blocks;
public ChunkSection(WorldChunk chunk, int y) {
this.blocks = new TexturedMaterial[ChunkSection.SIZE_XY][ChunkSection.SIZE_XY];
this.chunk = chunk;
this.y = y;
}
public int getY() {
return this.y;
}
public Chunk getChunk() {
return this.chunk;
}
public ArrayDensityVolume getDensityVolume() {
return this.densityVolume;
}
protected void setDensityVolume(ArrayDensityVolume densityVolume) {
this.densityVolume = densityVolume;
}
public Geometry getGeometry() {
return this.geometry;
}
public RigidBodyControl getRigidBodyControl() {
return this.rigidBodyControl;
}
public TexturedMaterial getBlock(int x, int y) {
return this.blocks[x][y];
}
void build() {
this.buildMesh();
this.buildCollisionMesh();
}
public void buildCollisionMesh() {
// we are going to build the collision mesh from the voxel data we have created whilst building
// the actual mesh.
// we are building the collision mesh separately for a few reasons:
// 1) Air voxels should not have collision data, so we can't simply duplicate the surface mesh.
// 2) We want to re-build the collision mesh every time a block has changed state, but we only want to modify
// the surface mesh (material type change or lighting change).
// remove the old rigidbody because it will either be renewed or not added at all if the collision mesh is empty.
if (Main.PHYSICS_ENABLED) {
if (this.rigidBodyControl != null) {
chunk.getGameWorld().getBulletAppState().getPhysicsSpace().remove(this.rigidBodyControl);
this.rigidBodyControl = null;
}
}
List<Vector3f> verts = new ArrayList<>();
List<Integer> indexes = new ArrayList<>();
int vJump = 0;
for (int x = 0; x < SIZE_XY; x++) {
for (int y = 0; y < SIZE_XY; y++) {
// float density = densityVolume.getDensity(x, y);
TexturedMaterial material = blocks[x][y];
if (material.getMaterial() != Material.AIR) {
verts.add(new Vector3f(x + 0, y + 0, 0));
verts.add(new Vector3f(x + 1, y + 0, 0));
verts.add(new Vector3f(x + 0, y + 1, 0));
verts.add(new Vector3f(x + 1, y + 1, 0));
indexes.add(vJump + 2);
indexes.add(vJump + 0);
indexes.add(vJump + 1);
indexes.add(vJump + 1);
indexes.add(vJump + 3);
indexes.add(vJump + 2);
vJump += 4;
}
}
}
// if the mesh is empty, we can just return here.
if (verts.isEmpty()) {
return;
}
Vector3f[] vertArray = new Vector3f[verts.size()];
vertArray = verts.toArray(vertArray);
int[] indexArray = new int[indexes.size()];
for (int i = 0; i < indexes.size(); i++) {
indexArray[i] = indexes.get(i);
}
Mesh mesh = new Mesh();
mesh.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(vertArray));
mesh.setBuffer(Type.Index, 3, BufferUtils.createIntBuffer(indexArray));
this.rigidBodyControl = new RigidBodyControl(new MeshCollisionShape(mesh), 0f);
this.geometry.addControl(this.rigidBodyControl);
// this.rigidBodyControl.setApplyPhysicsLocal(true); // we can maybe stop doing this...
this.rigidBodyControl.setFriction(2500f);
this.rigidBodyControl.setRestitution(0f);
this.rigidBodyControl.setPhysicsLocation(new Vector3f(
chunk.getGridX() * ChunkSection.SIZE_XY,
y << 4,
1));
// if the geometry has a parent, it means it's in the scene, so re-add the collision mesh too.
if (this.geometry.getParent() != null) {
if (Main.PHYSICS_ENABLED) {
chunk.getGameWorld().getBulletAppState().getPhysicsSpace().add(this.rigidBodyControl);
}
}
}
private void buildMesh() {
// we are going to include AIR as a physical block. This means that all meshes have an exact buffer count.
// This also means that we can simply modify the mesh instead of re-building it every time we break or place a
// block. While it does increase mesh size, it also speeds up mesh modification.
List<Vector3f> verts = new ArrayList<>();
List<Vector2f> texCoords = new ArrayList<>();
List<Integer> indexes = new ArrayList<>();
List<Vector2f> voxelData = new ArrayList<>();
int vJump = 0;
for (int x = 0; x < SIZE_XY; x++) {
for (int y = 0; y < SIZE_XY; y++) {
float density = densityVolume.getDensity(x, y);
verts.add(new Vector3f(x + 0, y + 0, 0));
verts.add(new Vector3f(x + 1, y + 0, 0));
verts.add(new Vector3f(x + 0, y + 1, 0));
verts.add(new Vector3f(x + 1, y + 1, 0));
texCoords.add(new Vector2f(x + 0, y + 0));
texCoords.add(new Vector2f(x + 1, y + 0));
texCoords.add(new Vector2f(x + 0, y + 1));
texCoords.add(new Vector2f(x + 1, y + 1));
indexes.add(vJump + 2);
indexes.add(vJump + 0);
indexes.add(vJump + 1);
indexes.add(vJump + 1);
indexes.add(vJump + 3);
indexes.add(vJump + 2);
vJump += 4;
Vector2f worldLocation = new Vector2f((chunk.getGridX() << 4) + x, (this.y << 4) + y);
if (density > 0.0f) {
// just set everything to dirt for now.
// all other materials and ores will be "injected" afterward.
// set the light to pitch black, and let the sun or light sources illuminate it.
voxelData.add(new Vector2f(Material.DIRT.getTextureId(), 0f));
voxelData.add(new Vector2f(Material.DIRT.getTextureId(), 0f));
voxelData.add(new Vector2f(Material.DIRT.getTextureId(), 0f));
voxelData.add(new Vector2f(Material.DIRT.getTextureId(), 0f));
blocks[x][y] = new TexturedMaterial(chunk, worldLocation, Material.DIRT);
}
else {
voxelData.add(new Vector2f(Material.AIR.getTextureId(), 0f));
voxelData.add(new Vector2f(Material.AIR.getTextureId(), 0f));
voxelData.add(new Vector2f(Material.AIR.getTextureId(), 0f));
voxelData.add(new Vector2f(Material.AIR.getTextureId(), 0f));
blocks[x][y] = new TexturedMaterial(chunk, worldLocation, Material.AIR);
}
blocks[x][y].setIndex(voxelData.size() - 4);
}
}
Vector3f[] vertArray = new Vector3f[verts.size()];
vertArray = verts.toArray(vertArray);
Vector2f[] texCoordArray = new Vector2f[texCoords.size()];
texCoordArray = texCoords.toArray(texCoordArray);
int[] indexArray = new int[indexes.size()];
for (int i = 0; i < indexes.size(); i++) {
indexArray[i] = indexes.get(i);
}
Vector2f[] voxelDataArray = new Vector2f[voxelData.size()];
voxelDataArray = voxelData.toArray(voxelDataArray);
Mesh mesh = new Mesh();
mesh.setBuffer(Type.Position, 3, BufferUtils.createFloatBuffer(vertArray));
mesh.setBuffer(Type.TexCoord, 2, BufferUtils.createFloatBuffer(texCoordArray));
mesh.setBuffer(Type.Index, 3, BufferUtils.createIntBuffer(indexArray));
mesh.setBuffer(Type.TexCoord2, 2, BufferUtils.createFloatBuffer(voxelDataArray));
// we can update the bound here, since the size will never change.
mesh.updateBound();
// the mesh is never null, since even air is rendered, albeit transparent.
this.geometry = new Geometry(String.format("Chunk: %d Section: %d", chunk.getGridX(), getY()), mesh);
this.geometry.setMaterial(chunk.getAssetLoader().getGroundMaterial());
this.geometry.setQueueBucket(Bucket.Transparent);
this.geometry.setLocalTranslation(new Vector3f(
chunk.getGridX() * ChunkSection.SIZE_XY,
y << 4,
1));
}
private void destroyMesh(Mesh mesh) {
for( VertexBuffer vb : mesh.getBufferList() ) {
BufferUtils.destroyDirectBuffer( vb.getData() );
}
}
protected void destroySurfaceGeometry() {
if (this.geometry != null) {
if (this.geometry.getParent() != null) {
this.geometry.removeFromParent();
}
if (Main.PHYSICS_ENABLED) {
chunk.getGameWorld().getBulletAppState().getPhysicsSpace().remove(this.rigidBodyControl);
}
this.rigidBodyControl = null;
destroyMesh(this.geometry.getMesh());
this.geometry = null;
}
}
}