package net.kennux.cubicworld.voxel; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.Map.Entry; import net.kennux.cubicworld.CubicWorld; import net.kennux.cubicworld.CubicWorldConfiguration; import net.kennux.cubicworld.inventory.IInventory; import net.kennux.cubicworld.inventory.IInventoryUpdateHandler; import net.kennux.cubicworld.math.Vector3i; import net.kennux.cubicworld.networking.packet.ClientChunkRequest; import net.kennux.cubicworld.networking.packet.inventory.ServerBlockInventoryUpdate; import net.kennux.cubicworld.voxel.handlers.IVoxelTileEntityHandler; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Camera; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.Mesh; import com.badlogic.gdx.graphics.VertexAttribute; import com.badlogic.gdx.graphics.VertexAttributes.Usage; import com.badlogic.gdx.graphics.g3d.Model; import com.badlogic.gdx.graphics.g3d.ModelBatch; import com.badlogic.gdx.graphics.g3d.ModelInstance; import com.badlogic.gdx.graphics.g3d.Renderable; import com.badlogic.gdx.graphics.g3d.RenderableProvider; import com.badlogic.gdx.graphics.glutils.ShaderProgram; import com.badlogic.gdx.math.Quaternion; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.math.Vector3; import com.badlogic.gdx.math.collision.BoundingBox; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.Disposable; import com.badlogic.gdx.utils.Pool; /** * Voxel chunk implementation. * * @author KennuX * */ public class VoxelChunk { // STATIC DATA private static final Vector3[] LEFT_SIDE_VERTICES = new Vector3[] { new Vector3(0, 0, 1), new Vector3(0, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 1, 1) }; private static final Vector3[] LEFT_SIDE_NORMALS = new Vector3[] { new Vector3(1, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 0) }; private static final short[] LEFT_SIDE_INDICES = new short[] { 1, 0, 2, 0, 3, 2 }; private static final Vector3[] RIGHT_SIDE_VERTICES = new Vector3[] { new Vector3(1, 0, 0), new Vector3(1, 0, 1), new Vector3(1, 1, 1), new Vector3(1, 1, 0), }; private static final Vector3[] RIGHT_SIDE_NORMALS = new Vector3[] { new Vector3(-1, 0, 0), new Vector3(-1, 0, 0), new Vector3(-1, 0, 0), new Vector3(-1, 0, 0) }; private static final short[] RIGHT_SIDE_INDICES = new short[] { 1, 0, 2, 0, 3, 2 }; private static final Vector3[] TOP_SIDE_VERTICES = new Vector3[] { new Vector3(0, 1, 0), new Vector3(1, 1, 0), new Vector3(1, 1, 1), new Vector3(0, 1, 1), }; private static final Vector3[] TOP_SIDE_NORMALS = new Vector3[] { new Vector3(0, 1, 0), new Vector3(0, 1, 0), new Vector3(0, 1, 0), new Vector3(0, 1, 0) }; private static final short[] TOP_SIDE_INDICES = new short[] { 1, 0, 2, 0, 3, 2 }; private static final Vector3[] BOTTOM_SIDE_VERTICES = new Vector3[] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 0, 1), new Vector3(0, 0, 1) }; private static final Vector3[] BOTTOM_SIDE_NORMALS = new Vector3[] { new Vector3(0, -1, 0), new Vector3(0, -1, 0), new Vector3(0, -1, 0), new Vector3(0, -1, 0) }; private static final short[] BOTTOM_SIDE_INDICES = new short[] { 1, 2, 0, 2, 3, 0 }; private static final Vector3[] BACK_SIDE_VERTICES = new Vector3[] { new Vector3(0, 0, 0), new Vector3(1, 0, 0), new Vector3(1, 1, 0), new Vector3(0, 1, 0) }; private static final Vector3[] BACK_SIDE_NORMALS = new Vector3[] { new Vector3(0, 0, -1), new Vector3(0, 0, -1), new Vector3(0, 0, -1), new Vector3(0, 0, -1) }; private static final short[] BACK_SIDE_INDICES = new short[] { 2, 1, 0, 0, 3, 2 }; private static final Vector3[] FRON_SIDE_VERTICES = new Vector3[] { new Vector3(1, 0, 1), new Vector3(0, 0, 1), new Vector3(0, 1, 1), new Vector3(1, 1, 1) }; private static final Vector3[] FRONT_SIDE_NORMALS = new Vector3[] { new Vector3(0, 0, 1), new Vector3(0, 0, 1), new Vector3(0, 0, 1), new Vector3(0, 0, 1) }; private static final short[] FRONT_SIDE_INDICES = new short[] { 2, 1, 0, 0, 3, 2 }; /** * <pre> * Rotation face mappings Indices: * 0 - LEFT * 1 - RIGHT * 2 - TOP * 3 - BOTTOM * 4 - BACK * 5 - FRONT * * Sides mapped from initial rotation. * </pre> */ public static final VoxelFace[][] ROTATION_MAPPINGS = new VoxelFace[][] { // Facing front new VoxelFace[] { VoxelFace.LEFT, VoxelFace.RIGHT, VoxelFace.TOP, VoxelFace.BOTTOM, VoxelFace.BACK, VoxelFace.FRONT }, // Facing right new VoxelFace[] { VoxelFace.FRONT, VoxelFace.BACK, VoxelFace.TOP, VoxelFace.BOTTOM, VoxelFace.LEFT, VoxelFace.RIGHT }, // Facing back new VoxelFace[] { VoxelFace.RIGHT, VoxelFace.LEFT, VoxelFace.TOP, VoxelFace.BOTTOM, VoxelFace.FRONT, VoxelFace.BACK }, // Facing left new VoxelFace[] { VoxelFace.BACK, VoxelFace.FRONT, VoxelFace.TOP, VoxelFace.BOTTOM, VoxelFace.RIGHT, VoxelFace.LEFT } }; /** * <pre> * The normal vectors for voxelfaces. * Example usage: * * VoxelFace f = VoxelFace.LEFT; * Vector3 normal = FACE_NORMALS[f.getValue()]; * * All vectors are 1 unit long!. * </pre> */ public static final Vector3[] FACE_NORMALS = new Vector3[] { new Vector3(1,0,0), new Vector3(-1,0,0), new Vector3(0,1,0), new Vector3(0,-1,0), new Vector3(0,0,1), new Vector3(0,0,-1), }; /** * Rotation quaternion mappings. * This quaternions will get used to define a voxel model's rendering rotation based on the rotation byte. * * They get calculated in the constructor. */ public static Quaternion[] rotationTransformMappings; // END OF STATIC DATA /** * The current voxel mesh used for rendering. */ private Mesh voxelMesh; /** * Gets used to synchronize voxel data changes. */ private Object voxelDataLockObject = new Object(); /** * If this flag is set to true the voxel mesh got marked as dirty, which * means it needs to get re-generated. */ private boolean voxelMeshDirty; /** * If this flag is set to true the new mesh data gots generated by the * update thread. */ private boolean newMeshDataReady; /** * The voxel mesh's bounding box. */ private BoundingBox boundingBox; /** * The voxel mesh's new bounding box. */ private BoundingBox newBoundingBox; /** * The voxel data (voxeltype id's, -1 for no block here). You must lock * voxelDataLockObject when you are writing to this field. After direct * writing to this you must call setDirty(). */ private volatile VoxelData[][][] voxelData; /** * The chunk x-position. */ private int chunkX; /** * The chunk y-position. */ private int chunkY; /** * The chunk z-position. */ private int chunkZ; /** * Gets set to true after the world generation is done. */ private boolean generationDone = false; /** * Gets set to true if the lighting information is up 2 date. */ private boolean lightingDirty = false; /** * Gets set to true if a save of this chunk to the voxel world file is * needed. * Will get handled in update(). */ private boolean saveDirty = false; /** * The voxel world master instance. */ public VoxelWorld master; /** * The models which will get rendered in every frame. * They get built in generateMeshData(). */ private ArrayList<ModelInstance> models; /** * The temporary new models value. * Will get built in generateMeshData(). * * This gets first set and then used in the models arraylist. */ private ArrayList<ModelInstance> newModels; /** * The voxel update handlers list. */ private HashMap<Vector3, IVoxelTileEntityHandler> tileEntityHandlers = new HashMap<Vector3, IVoxelTileEntityHandler>(); private Object generationLockObject = new Object(); // New mesh data list // Gets generated in the update() function which gets called by an own // thread separated from the main thread. private float[] newVertices; private short[] newIndices; private final int vertexSize = 6; /** * The vertex count of the current mesh. */ private int vertexCount; /** * The absolute chunk position vector. */ private Vector3i absoluteChunkPosition; /** * The mesh pool fifo stack. */ private static LinkedList<Mesh> meshPool = new LinkedList<Mesh>(); /** * The last call id when update() was called. * Used to limit chunk updates per frame for lag reduction. */ private static long lastUpdateCallId = -1; /** * Contains the number of generateMesh() calls this frame. */ private static int generationsProcessedThisFrame = -1; /** * The last frame id when render() was called. * Used to limit chunk updates per frame for lag reduction. */ private static long lastRenderFrameId = -1; /** * Contains the number of generateMesh() calls this frame. */ private static int creationsProcessedThisFrame = -1; private static Mesh newMesh() { return new Mesh(Mesh.VertexDataType.VertexBufferObject, false, 16368, 16368, new VertexAttribute(Usage.Position, 3, "v_Position"), new VertexAttribute(Usage.TextureCoordinates, 2, "v_Uv"), /*new VertexAttribute(Usage.Normal, 3, "v_Normal"),*/ new VertexAttribute(Usage.ColorUnpacked, 1, "v_Light")); } public VoxelChunk(int chunkX, int chunkY, int chunkZ, VoxelWorld master) { this.voxelData = null; // Init rotation mappings rotationTransformMappings = new Quaternion[] { // Facing front new Quaternion(0, 0, 0, 0), // Facing right new Quaternion(0, 0, 0, 0).setEulerAngles(90, 0, 0), // Facing back new Quaternion(0, 0, 0, 0).setEulerAngles(180, 0, 0), // Facing left new Quaternion(0, 0, 0, 0).setEulerAngles(270, 0, 0) }; // Set chunkspae position this.chunkX = chunkX; this.chunkY = chunkY; this.chunkZ = chunkZ; // Set absolute chunk position this.absoluteChunkPosition = new Vector3i(this.chunkX * VoxelWorld.chunkWidth, this.chunkY * VoxelWorld.chunkHeight, this.chunkZ * VoxelWorld.chunkDepth); this.master = master; } /** * Sets this chunk to dirty which will regenerate the mesh data. * ONLY call this if you loaded the chunk data from the world file. */ private void chunkDataWasLoaded() { this.lightingDirty = true; this.voxelMeshDirty = true; this.saveDirty = false; } /** * Causes this chunk to regenerate it's mesh. * Will only recalculate lighting and the mesh. */ public void regenerateChunkMesh() { this.lightingDirty = true; this.voxelMeshDirty = true; this.saveDirty = false; } /** * Sets this chunk to dirty which will regenerate the mesh data. You have to * call this function after direct writing to voxelData. */ private void chunkDataWasModified() { this.lightingDirty = true; this.voxelMeshDirty = true; this.saveDirty = true; } /** * Creates the new mesh from newVertices, newUvs, newColors and newIndices * lists. */ private void createNewMesh() { synchronized (this.generationLockObject) { // Mesh empty? if (this.newVertices.length == 0) { this.voxelMesh = null; } else { // Construct new mesh if (this.voxelMesh == null) { this.voxelMesh = meshPool.poll(); if (this.voxelMesh == null) { this.voxelMesh = newMesh(); } } // Set the vertices this.voxelMesh.setVertices(this.newVertices); this.voxelMesh.setIndices(this.newIndices); // Set vertex count this.vertexCount = this.newVertices.length; // Calculate bounding box try { // DISABLED DUE TO HIGH PERFORMANCE COST // INSTEAD STATIC BOUNDING BOXES WILL GET USED GENERATED IN generateMesh() // this.boundingBox = newMesh.calculateBoundingBox(); this.boundingBox = this.newBoundingBox; } catch (Exception e) { // Happens if there are no vertices in this mesh, so just ignore this error. } } this.newMeshDataReady = false; this.voxelMeshDirty = false; // Free old data this.newBoundingBox = null; this.newVertices = null; this.newIndices = null; // Dispose old mesh if available /*if (this.voxelMesh != null) this.voxelMesh.dispose();*/ this.models = this.newModels; // this.voxelMesh = newMesh; } } /** * This call is not thread-safe, only call this on an object which wont get used anymore. * * This will not get called by the garbage collector as this class does not implement the disposable interface. * It is only used to add the current mesh to the mesh pool. */ public void dispose() { meshPool.add(this.voxelMesh); } /** * Generates the voxel mesh based on the current chunk's voxel data. */ private void generateMesh() { synchronized (this.generationLockObject) { if (this.lightingDirty || this.voxelData == null) return; // The local copy of the voxel data object VoxelData[][][] voxelData = this.getVoxelData(); // the vertices array list final int initListLength = 16000; // Start with a length of 16000 to avoid re-allocation ArrayList<Float> vertices = new ArrayList<Float>(initListLength * vertexSize); ArrayList<Short> indices = new ArrayList<Short>(initListLength); ArrayList<ModelInstance> modelList = new ArrayList<ModelInstance>(); short indicesCounter = 0; for (int x = 0; x < VoxelWorld.chunkWidth; x++) { for (int z = 0; z < VoxelWorld.chunkDepth; z++) { for (int y = 0; y < VoxelWorld.chunkHeight; y++) { // Voxel in my position? if (voxelData[x][y][z] == null || voxelData[x][y][z].voxelType == null) continue; Vector3i absolutePos = this.getAbsoluteVoxelPosition(x, y, z); int absX = absolutePos.x; int absY = absolutePos.y; int absZ = absolutePos.z; VoxelData leftVoxel = (x == 0 ? this.master.getVoxel(absX-1, absY, absZ) : voxelData[x-1][y][z]); VoxelData rightVoxel = (x == VoxelWorld.chunkWidth-1 ? this.master.getVoxel(absX+1, absY, absZ) : voxelData[x+1][y][z]); VoxelData topVoxel = (y == VoxelWorld.chunkHeight-1 ? this.master.getVoxel(absX, absY+1, absZ) : voxelData[x][y+1][z]); VoxelData bottomVoxel = (y == 0 ? this.master.getVoxel(absX, absY-1, absZ) : voxelData[x][y-1][z]); VoxelData backVoxel = (z == 0 ? this.master.getVoxel(absX, absY, absZ-1) : voxelData[x][y][z-1]); VoxelData frontVoxel = (z == VoxelWorld.chunkDepth-1 ? this.master.getVoxel(absX, absY, absZ+1) : voxelData[x][y][z+1]); boolean leftSideVisible = x != 0 ? (leftVoxel == null || leftVoxel.voxelType == null || leftVoxel.voxelType.voxelId < 0 || leftVoxel.voxelType.transparent) : true; boolean rightSideVisible = x != VoxelWorld.chunkWidth-1 ? (rightVoxel == null || rightVoxel.voxelType == null || rightVoxel.voxelType.voxelId < 0 || rightVoxel.voxelType.transparent) : true; boolean topSideVisible = y != VoxelWorld.chunkHeight-1 ? (topVoxel == null || topVoxel.voxelType == null || topVoxel.voxelType.voxelId < 0 || topVoxel.voxelType.transparent) : true; boolean bottomSideVisible = y != 0 ? (bottomVoxel == null || bottomVoxel.voxelType == null || bottomVoxel.voxelType.voxelId < 0 || bottomVoxel.voxelType.transparent) : true; boolean backSideVisible = z != 0 ? (backVoxel == null || backVoxel.voxelType == null || backVoxel.voxelType.voxelId < 0 || backVoxel.voxelType.transparent) : true; boolean frontSideVisible = z != VoxelWorld.chunkDepth-1 ? (frontVoxel == null || frontVoxel.voxelType == null || frontVoxel.voxelType.voxelId < 0 || frontVoxel.voxelType.transparent) : true; // Model or normal voxel rendering? if (voxelData[x][y][z].voxelType.isModelRendering() && // Atleast any side visible? (leftSideVisible || rightSideVisible || topSideVisible || bottomSideVisible || backSideVisible || frontSideVisible)) { // Model rendering Model m = voxelData[x][y][z].voxelType.getModel(); ModelInstance mInstance = new ModelInstance(m); Vector3 vert = new Vector3(x, y, z); vert.x += 0.5f + ((float) this.chunkX * (float) VoxelWorld.chunkWidth); vert.y += 0.5f + ((float) this.chunkY * (float) VoxelWorld.chunkHeight); vert.z += 0.5f + ((float) this.chunkZ * (float) VoxelWorld.chunkDepth); // Set model transformation mInstance.transform.set(vert, VoxelChunk.rotationTransformMappings[voxelData[x][y][z].rotation]); modelList.add(mInstance); } else { // Normal voxel rendering VoxelFace[] faceMappings = ROTATION_MAPPINGS[voxelData[x][y][z].rotation]; byte leftLighting = leftVoxel == null ? 0 : leftVoxel.lightLevel; byte rightLighting = rightVoxel == null ? 0 : rightVoxel.lightLevel; byte topLighting = topVoxel == null ? 0 : topVoxel.lightLevel; byte bottomLighting = bottomVoxel == null ? 0 : bottomVoxel.lightLevel; byte backLighting = backVoxel == null ? 0 : backVoxel.lightLevel; byte frontLighting = frontVoxel == null ? 0 : frontVoxel.lightLevel; // Write mesh data if (leftSideVisible) { this.WriteSideData(vertices, indices, LEFT_SIDE_VERTICES, LEFT_SIDE_NORMALS, LEFT_SIDE_INDICES, indicesCounter, x, y, z, voxelData[x][y][z], faceMappings[0], leftLighting); indicesCounter += LEFT_SIDE_VERTICES.length; } if (rightSideVisible) { this.WriteSideData(vertices, indices, RIGHT_SIDE_VERTICES, RIGHT_SIDE_NORMALS, RIGHT_SIDE_INDICES, indicesCounter, x, y, z, voxelData[x][y][z], faceMappings[1], rightLighting); indicesCounter += RIGHT_SIDE_VERTICES.length; } if (topSideVisible) { this.WriteSideData(vertices, indices, TOP_SIDE_VERTICES, TOP_SIDE_NORMALS, TOP_SIDE_INDICES, indicesCounter, x, y, z, voxelData[x][y][z], faceMappings[2], topLighting); indicesCounter += TOP_SIDE_VERTICES.length; } if (bottomSideVisible) { this.WriteSideData(vertices, indices, BOTTOM_SIDE_VERTICES, BOTTOM_SIDE_NORMALS, BOTTOM_SIDE_INDICES, indicesCounter, x, y, z, voxelData[x][y][z], faceMappings[3], bottomLighting); indicesCounter += BOTTOM_SIDE_VERTICES.length; } if (backSideVisible) { this.WriteSideData(vertices, indices, BACK_SIDE_VERTICES, BACK_SIDE_NORMALS, BACK_SIDE_INDICES, indicesCounter, x, y, z, voxelData[x][y][z], faceMappings[4], backLighting); indicesCounter += BACK_SIDE_VERTICES.length; } if (frontSideVisible) { this.WriteSideData(vertices, indices, FRON_SIDE_VERTICES, FRONT_SIDE_NORMALS, FRONT_SIDE_INDICES, indicesCounter, x, y, z, voxelData[x][y][z], faceMappings[5], frontLighting); indicesCounter += FRON_SIDE_VERTICES.length; } } } } } // Set new models list and bounding box this.newModels = modelList; this.newBoundingBox = new BoundingBox(this.getAbsoluteVoxelPosition(0, 0, 0).toFloatVector(), this.getAbsoluteVoxelPosition(VoxelWorld.chunkWidth, VoxelWorld.chunkHeight, VoxelWorld.chunkDepth).toFloatVector()); // Generate vertex data this.newVertices = new float[vertices.size()]; for (int i = 0; i < this.newVertices.length; i++) { this.newVertices[i] = vertices.get(i).floatValue(); } // Build indices this.newIndices = new short[indices.size()]; int i = 0; for (Short index : indices) { this.newIndices[i] = index.shortValue(); i++; } this.newMeshDataReady = true; } } public Vector3i getAbsoluteVoxelPosition(int x, int y, int z) { return new Vector3i(x + this.absoluteChunkPosition.x, y + this.absoluteChunkPosition.y, z + this.absoluteChunkPosition.z); } /** * Returns the bounding box for a voxel given in local voxelspace. * If there is no voxel at this position, null will be returned. * If the voxel is a non-collidable (like vegetation), also null is * returned. * * @param x * @param y * @param z * @return */ public BoundingBox getBoundingBox(int x, int y, int z) { // Bounds check if (this.voxelData != null && x >= 0 && y >= 0 && z >= 0 && x < VoxelWorld.chunkWidth && y < VoxelWorld.chunkHeight && z < VoxelWorld.chunkDepth && this.hasVoxel(x, y, z)) { return new BoundingBox(new Vector3(x, y, z), new Vector3(x + 1, y + 1, z + 1)); } // Not found! return null; } /** * Returns the light level of the block at the given position. * Returns -1 if there is no voxel in the given position or if an error * happend. * * @param x * @param y * @param z * @return */ public byte getLightLevel(int x, int y, int z) { // Bounds check if (this.voxelData != null && x >= 0 && y >= 0 && z >= 0 && x < VoxelWorld.chunkWidth && y < VoxelWorld.chunkHeight && z < VoxelWorld.chunkDepth) { synchronized (this.voxelDataLockObject) { if (this.voxelData[x][y][z] != null) return this.voxelData[x][y][z].lightLevel; } } // Not found! return -1; } /** * Gets the voxel data at the given x|y|z position. * Returns null in the case of an error. * * @param x * @param y * @param z * @param voxel * @return */ public VoxelData getVoxel(int x, int y, int z) { // Bounds check if (this.voxelData != null && x >= 0 && y >= 0 && z >= 0 && x < VoxelWorld.chunkWidth && y < VoxelWorld.chunkHeight && z < VoxelWorld.chunkDepth) { synchronized (this.voxelDataLockObject) { return this.voxelData[x][y][z]; } } // Not found! return null; } /** * Returns a copy of the voxel data array. * Only the array object is a copy, the voxeldata contents of the array are * references. * * @return */ public VoxelData[][][] getVoxelData() { synchronized (this.voxelDataLockObject) { return this.voxelData.clone(); } } /** * Checks if the the voxel data at the given x|y|z position is not null or * type id -1. Returns false in the case of an error. * * @param x * @param y * @param z * @param voxel * @return */ public boolean hasVoxel(int x, int y, int z) { // Bounds check if (this.voxelData != null && x >= 0 && y >= 0 && z >= 0 && x < VoxelWorld.chunkWidth && y < VoxelWorld.chunkHeight && z < VoxelWorld.chunkDepth) { synchronized (this.voxelDataLockObject) { return this.voxelData[x][y][z] != null && this.voxelData[x][y][z].voxelType != null; } } // Not found! return false; } /** * Returns true if the world generator already generated this chunk. * * @return */ public boolean isGenerationDone() { return generationDone; } /** * Returns true if this voxel chunk is ready for rendering (this means, lighting is done and mesh is generated). * @return */ public boolean isReadyForRendering() { return this.isInitializedAndLightingReady() && !this.voxelMeshDirty && !this.newMeshDataReady; } /** * Chunk gets initialized after data got set by the generator or it was * loaded from hdd * * @return */ public boolean isInitialized() { return this.voxelData != null && this.isGenerationDone(); } /** * Returns true if this chunk is initialized and it's lighting is not dirty. * @return */ public boolean isInitializedAndLightingReady() { return this.isInitialized() && !this.lightingDirty; } /** * Loads the voxel data from the voxel chunk file. */ public void loadVoxelData() { if (this.master.hasWorldFile() && this.master.getWorldFile().hasChunk(chunkX, chunkY, chunkZ)) { synchronized (this.voxelDataLockObject) { // Read this.voxelData = this.master.getWorldFile().readChunk(chunkX, chunkY, chunkZ); this.setInventoryUpdateHandlerAll(); this.chunkDataWasLoaded(); this.setGenerationDone(true); this.setTileEntityHandlerAll(); } } } /** * <pre> * Renders this chunk. Will do nothing if the current voxel mesh is not * available yet. * * Otherwise it will just call the render() method of the chunk mesh object. * * This method is <b>NOT</b> thread-safe. * </pre> */ public void render(Camera cam, ShaderProgram shader) { CubicWorld.getClient().profiler.startProfiling("MeshCreation"+this.chunkX+"|"+this.chunkY+"|"+this.chunkZ, ""); boolean frameMismatch = lastRenderFrameId != Gdx.graphics.getFrameId(); if (this.voxelMeshDirty && this.newMeshDataReady && (CubicWorldConfiguration.meshCreationsPerFrameLimit == -1 || frameMismatch || creationsProcessedThisFrame <= CubicWorldConfiguration.meshCreationsPerFrameLimit)) { // If the frame ids mismatch if (frameMismatch) { // Update frame id and reset counter lastRenderFrameId = Gdx.graphics.getFrameId(); creationsProcessedThisFrame = 0; } this.createNewMesh(); creationsProcessedThisFrame++; } CubicWorld.getClient().profiler.stopProfiling("MeshCreation"+this.chunkX+"|"+this.chunkY+"|"+this.chunkZ); CubicWorld.getClient().profiler.startProfiling("MeshRendering"+this.chunkX+"|"+this.chunkY+"|"+this.chunkZ, ""); if (this.voxelMesh != null && this.boundingBox != null && cam.frustum.boundsInFrustum(this.boundingBox)) { // Render chunk mesh this.voxelMesh.render(shader, GL20.GL_TRIANGLES); } CubicWorld.getClient().profiler.stopProfiling("MeshRendering"+this.chunkX+"|"+this.chunkY+"|"+this.chunkZ); } /** * <pre> * Renders all voxels on this chunk which are model rendered. * This is part of the second rendering pass of the voxel world. * * This method is <b>NOT</b> thread-safe. * </pre> * * @param modelBatch */ public void renderModels(Camera cam, ModelBatch modelBatch) { if (this.voxelMesh != null && this.boundingBox != null && cam.frustum.boundsInFrustum(this.boundingBox)) { // Render block models for (ModelInstance mI : this.models) { modelBatch.render(mI); } } } /** * Call this with true as parameter after the generation of the chunk is * done. * * @param generationDone */ public void setGenerationDone(boolean generationDone) { this.generationDone = generationDone; } /** * Helper function for setting the inventory update handler. * * @param positionX * @param positionY * @param positionZ * @param inventory */ private void setInventoryUpdateHandler(int positionX, int positionY, int positionZ, VoxelData inventoryVoxel) { if (this.master.isServer() && inventoryVoxel != null && inventoryVoxel.blockInventory != null) { // Declare final positions final int posX = positionX; final int posY = positionY; final int posZ = positionZ; final VoxelChunk chunkInstance = this; inventoryVoxel.blockInventory.setUpdateHandler(new IInventoryUpdateHandler() { @Override public void inventoryGotUpdated(IInventory inventory) { ServerBlockInventoryUpdate updatePacket = new ServerBlockInventoryUpdate(); updatePacket.inventory = inventory; updatePacket.voxelPositionX = posX; updatePacket.voxelPositionY = posY; updatePacket.voxelPositionZ = posZ; updatePacket.setCullPosition(new Vector3(posX, posY, posZ)); CubicWorld.getServer().addPacket(updatePacket); chunkInstance.chunkDataWasModified(); } }); } } /** * This function will call the setInventoryUpdateHandler for every voxel data which has a inventory attached. * * @see VoxelChunk#setInventoryUpdateHandler(int, int, int, VoxelData) */ private void setInventoryUpdateHandlerAll() { if (!this.master.isServer()) return; for (int x = 0; x < VoxelWorld.chunkWidth; x++) for (int y = 0; y < VoxelWorld.chunkHeight; y++) for (int z = 0; z < VoxelWorld.chunkDepth; z++) if (this.voxelData[x][y][z] != null && this.voxelData[x][y][z].blockInventory != null) { Vector3i absolutePos = this.getAbsoluteVoxelPosition(x, y, z); this.setInventoryUpdateHandler(absolutePos.x, absolutePos.y, absolutePos.z, this.voxelData[x][y][z]); } } /** * Sets the voxel data at the given x|y|z position. * Pass in null or just new VoxelData() as voxel for setting air. * * @param x * @param y * @param z * @param voxel */ public void setVoxel(int x, int y, int z, VoxelData voxel) { synchronized (this.voxelDataLockObject) { if (this.voxelData == null) return; // if voxel is null, create air voxel if (voxel == null) voxel = new VoxelData(); // Remove update handler if existing Vector3 voxelPos = new Vector3(x, y, z); this.tileEntityHandlers.remove(voxelPos); this.voxelData[x][y][z] = voxel; // Inventory if (voxel != null && voxel.blockInventory != null) { Vector3i absolutePos = this.getAbsoluteVoxelPosition(x, y, z); this.setInventoryUpdateHandler(absolutePos.x, absolutePos.y, absolutePos.z, voxel); } this.chunkDataWasModified(); // notify about update if (this.master.getVoxelDataUpdateHandler() != null) { Vector3i absolutePos = this.getAbsoluteVoxelPosition(x, y, z); this.master.getVoxelDataUpdateHandler().handleVoxelDataUpdate(absolutePos.x, absolutePos.y, absolutePos.z, voxel); } // Update voxel handlers map if (voxel != null && voxel.voxelType != null) { if (voxel.voxelType.isTileEntity() && voxel.tileEntity != null) { this.tileEntityHandlers.put(voxelPos, voxel.tileEntity); } } } } /** * Sets the voxel data array. * * @param voxelData */ public void setVoxelData(VoxelData[][][] voxelData) { synchronized (this.voxelDataLockObject) { // Iterate through all voxel datas and set null voxels to air voxels for (int x = 0; x < voxelData.length; x++) for (int y = 0; y < voxelData[x].length; y++) for (int z = 0; z < voxelData[x][y].length; z++) if (voxelData[x][y][z] == null) voxelData[x][y][z] = new VoxelData(); this.voxelData = voxelData; this.setInventoryUpdateHandlerAll(); this.chunkDataWasModified(); this.setGenerationDone(true); this.setTileEntityHandlerAll(); } } /** * Iterates through every voxel data in this instance and collects all update handler. */ private void setTileEntityHandlerAll() { synchronized (this.voxelDataLockObject) { this.tileEntityHandlers.clear(); // Iterate through all voxel data instances for (int x = 0; x < this.voxelData.length; x++) for (int y = 0; y < this.voxelData[x].length; y++) for (int z = 0; z < this.voxelData[x][y].length; z++) // Check if the voxel at the given position is not null, not air and a tile entity if (this.voxelData[x][y][z] != null && this.voxelData[x][y][z].voxelType != null && this.voxelData[x][y][z].voxelType.isTileEntity()) // Add to tile entity handlers this.tileEntityHandlers.put(new Vector3(x, y, z), this.voxelData[x][y][z].tileEntity); } } /** * Simulates the current chunk. Currently this doesnt do anything but it * will be called once per tick (ticksPerSecond = fps on client, target * ticks on server are 20). */ public void simulate() { // TODO Fluid Simulation } /** * <pre> * Updates this voxel chunk instance. * Does the following: * * - Checks if save to hdd needed * -> If yes, it will save to hdd * - Checks if the lighting is dirty and needs a regeneration * -> If yes, it regenerates it. * - Checks if the mesh is dirty * -> If yes, it regenerates it. * </pre> */ public void update() { // Save needed? if (this.saveDirty && this.master.hasWorldFile() && this.isInitialized()) { synchronized (this.voxelDataLockObject) { // Save chunk this.master.getWorldFile().writeChunk(this.chunkX, this.chunkY, this.chunkZ, this.voxelData); this.saveDirty = false; } } // Lighting check // The lighting algorithm needs all chunks around this chunk to be ready. if (!ClientChunkRequest.areRequestsPending() && this.lightingDirty && this.isInitialized() && (this.chunkY == this.master.chunksOnYAxis() || this.master.chunkLightingReady(new ChunkKey(this.chunkX, this.chunkY+1, this.chunkZ)))) { this.recalculateLighting(); } synchronized (this.voxelDataLockObject) { HashMap<Vector3, IVoxelTileEntityHandler> tileEntitiesClone = (HashMap<Vector3, IVoxelTileEntityHandler>) this.tileEntityHandlers.clone(); // Exec tile entity updates for (Entry<Vector3, IVoxelTileEntityHandler> entry : tileEntitiesClone.entrySet()) { int x = (int) entry.getKey().x; int y = (int) entry.getKey().y; int z = (int) entry.getKey().z; Vector3i absolutePosition = this.getAbsoluteVoxelPosition((int) entry.getKey().x, (int) entry.getKey().y, (int) entry.getKey().z); VoxelData voxelData = this.getVoxel(x, y, z); entry.getValue().handleUpdate(voxelData, absolutePosition.x, absolutePosition.y, absolutePosition.z, this.master.isServer()); } } boolean frameMismatch = (lastUpdateCallId != this.master.updateCallId); if (!ClientChunkRequest.areRequestsPending() && !this.lightingDirty && this.voxelMeshDirty && this.generationDone && !this.master.isServer() && // Check all neighbours if lighting is ready this.master.chunkLightingReady(this.chunkX+1, this.chunkY, this.chunkZ) && this.master.chunkLightingReady(this.chunkX-1, this.chunkY, this.chunkZ) && this.master.chunkLightingReady(this.chunkX, this.chunkY+1, this.chunkZ) && this.master.chunkLightingReady(this.chunkX, this.chunkY-1, this.chunkZ) && this.master.chunkLightingReady(this.chunkX, this.chunkY, this.chunkZ+1) && this.master.chunkLightingReady(this.chunkX, this.chunkY, this.chunkZ-1) && (CubicWorldConfiguration.meshGenerationsPerFrameLimit == -1 || frameMismatch || generationsProcessedThisFrame <= CubicWorldConfiguration.meshGenerationsPerFrameLimit)) { // If the frame ids mismatch if (frameMismatch) { // Update frame id and reset counter lastUpdateCallId = this.master.updateCallId; generationsProcessedThisFrame = 0; } this.generateMesh(); generationsProcessedThisFrame++; } } /** * Calculates the light for the given position and face of a voxel. */ private void recalculateLighting() { synchronized (this.voxelDataLockObject) { if (this.voxelData == null) return; for (int x = VoxelWorld.chunkWidth-1; x >= 0; x--) for (int y = VoxelWorld.chunkHeight-1; y >= 0; y--) for (int z = VoxelWorld.chunkDepth-1; z >= 0; z--) if (this.voxelData[x][y][z] != null) { VoxelData v = this.voxelData[x][y][z]; Vector3i absolutePos = this.getAbsoluteVoxelPosition(x, y, z); int absX = absolutePos.x; int absY = absolutePos.y; int absZ = absolutePos.z; if (y == VoxelWorld.chunkHeight-1 && (v.voxelType == null || v.voxelType.transparent)) { v.lightLevel = (byte) (CubicWorldConfiguration.maxLightLevel - (this.master.chunksOnYAxis() - this.chunkY)); } else { // Simplified: // (y == chunkHeight-1 [last block on y-axis top] ? get light level from master in global space : (voxel data in local space is not set ? max light level : get from local space)) byte topLightLevel = (y == VoxelWorld.chunkHeight - 1 ? this.master.getLightLevel(absX, absY+1, absZ) : (this.voxelData[x][y+1][z] == null ? CubicWorldConfiguration.maxLightLevel : this.voxelData[x][y+1][z].lightLevel)); // Forward light through air and transparent blocks if (v.voxelType == null || v.voxelType.transparent) v.lightLevel = topLightLevel; else { // Get neighbour levels byte leftLightLevel = (x == 0 ? this.master.getLightLevel(absX-1, absY, absZ) : (this.voxelData[x-1][y][z] == null ? CubicWorldConfiguration.maxLightLevel : this.voxelData[x-1][y][z].lightLevel)); byte rightLightLevel = (x == VoxelWorld.chunkWidth - 1 ? this.master.getLightLevel(absX+1, absY, absZ) : (this.voxelData[x+1][y][z] == null ? CubicWorldConfiguration.maxLightLevel : this.voxelData[x+1][y][z].lightLevel)); byte backLightLevel = (z == 0 ? this.master.getLightLevel(absX, absY, absZ-1) : (this.voxelData[x][y][z-1] == null ? CubicWorldConfiguration.maxLightLevel : this.voxelData[x][y][z-1].lightLevel)); byte frontLightLevel = (z == VoxelWorld.chunkDepth - 1 ? this.master.getLightLevel(absX, absY, absZ+1) : (this.voxelData[x][y][z+1] == null ? CubicWorldConfiguration.maxLightLevel : this.voxelData[x][y][z+1].lightLevel)); byte[] adjacentLightLevels = new byte[] { topLightLevel, leftLightLevel, rightLightLevel, backLightLevel, frontLightLevel }; Arrays.sort(adjacentLightLevels); byte highestLevel = adjacentLightLevels[4]; byte lightLevel = (byte) (highestLevel-1); if (lightLevel <= 0) { lightLevel = 1; } v.lightLevel = lightLevel; } } } this.lightingDirty = false; } } /** * Writes mesh data to the given lists. * * @param vertices Main vertex list. * @param indices Main index list. * @param uvs Main uvs list. * @param colors Main colors list. * @param sideVertices Vertex array from the side vertices array. * @param sideIndices Side indices from the side indices array. * @param indicesCounter The current index counter. * @param x The current voxel worldspace position. * @param y The current voxel worldspace position. * @param z The current voxel worldspace position. * @param color The voxel color. * @param blockId The voxel type id. * @param face The foxel face to use for getting uv coordinates. */ private void WriteSideData(ArrayList<Float> vertices, ArrayList<Short> indices, Vector3[] sideVertices, Vector3[] sideNormals, short[] sideIndices, short indicesCounter, int x, int y, int z, VoxelData voxelData, VoxelFace face, byte lightLevel) { // short blockId = voxelData.voxelType.voxelId; Vector2[] uv = voxelData.getRenderState().getUvsForFace(face); // boolean transparent = VoxelEngine.getVoxelType(blockId).transparent; // Calculate absolute vertex index count. for (int i = 0; i < sideIndices.length; i++) { indices.add((short) (indicesCounter + sideIndices[i])); } float lightValue = lightLevel / (float) CubicWorldConfiguration.maxLightLevel; // Transform vertices based on the block's position. for (int i = 0; i < sideVertices.length; i++) { float vertX = sideVertices[i].x + x + ((float) this.chunkX * (float) VoxelWorld.chunkWidth); float vertY = sideVertices[i].y + y + ((float) this.chunkY * (float) VoxelWorld.chunkHeight); float vertZ = sideVertices[i].z + z + ((float) this.chunkZ * (float) VoxelWorld.chunkDepth); vertices.add(vertX); vertices.add(vertY); vertices.add(vertZ); vertices.add(uv[i].x); vertices.add(uv[i].y); vertices.add(lightValue); } } }