diff --git a/src/main/java/electrosphere/client/fluid/cells/FluidCellManager.java b/src/main/java/electrosphere/client/fluid/cells/FluidCellManager.java index ef4556ba..6b856f64 100644 --- a/src/main/java/electrosphere/client/fluid/cells/FluidCellManager.java +++ b/src/main/java/electrosphere/client/fluid/cells/FluidCellManager.java @@ -404,7 +404,7 @@ public class FluidCellManager { * @return The chunk data at the specified points */ ChunkData getChunkDataAtPoint(int worldX, int worldY, int worldZ){ - return Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldX,worldY,worldZ); + return Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldX,worldY,worldZ,ChunkData.NO_STRIDE); } diff --git a/src/main/java/electrosphere/client/foliagemanager/FoliageCell.java b/src/main/java/electrosphere/client/foliagemanager/FoliageCell.java index 2205ad8d..7b652ab8 100644 --- a/src/main/java/electrosphere/client/foliagemanager/FoliageCell.java +++ b/src/main/java/electrosphere/client/foliagemanager/FoliageCell.java @@ -205,7 +205,7 @@ public class FoliageCell { protected void generate(){ boolean shouldGenerate = false; //get foliage types supported - ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPosition); + ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPosition,ChunkData.NO_STRIDE); if(data == null){ return; } diff --git a/src/main/java/electrosphere/client/foliagemanager/FoliageChunk.java b/src/main/java/electrosphere/client/foliagemanager/FoliageChunk.java index 67979564..de56289b 100644 --- a/src/main/java/electrosphere/client/foliagemanager/FoliageChunk.java +++ b/src/main/java/electrosphere/client/foliagemanager/FoliageChunk.java @@ -77,9 +77,9 @@ public class FoliageChunk { */ public void initCells(){ Globals.profiler.beginCpuSample("FoliageChunk.initCells"); - this.currentChunkData = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos); + this.currentChunkData = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos,ChunkData.NO_STRIDE); // //evaluate top cells if chunk above this one exists - this.aboveChunkData = Globals.clientTerrainManager.getChunkDataAtWorldPoint(new Vector3i(worldPos).add(0,1,0)); + this.aboveChunkData = Globals.clientTerrainManager.getChunkDataAtWorldPoint(new Vector3i(worldPos).add(0,1,0),ChunkData.NO_STRIDE); this.updateCells(true); Globals.profiler.endCpuSample(); } @@ -113,7 +113,7 @@ public class FoliageChunk { * @return true if contains foliage voxel, false otherwise */ private boolean checkContainsFoliageVoxel(){ - ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(this.getWorldPos()); + ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(this.getWorldPos(),ChunkData.NO_STRIDE); if(data == null){ return false; } diff --git a/src/main/java/electrosphere/client/terrain/cache/ChunkData.java b/src/main/java/electrosphere/client/terrain/cache/ChunkData.java index 4036d6a0..3c9f5762 100644 --- a/src/main/java/electrosphere/client/terrain/cache/ChunkData.java +++ b/src/main/java/electrosphere/client/terrain/cache/ChunkData.java @@ -12,6 +12,11 @@ import electrosphere.server.terrain.manager.ServerTerrainChunk; */ public class ChunkData { + /** + * No stride + */ + public static final int NO_STRIDE = 0; + //The size of a chunk in virtual data public static final int CHUNK_SIZE = ServerTerrainChunk.CHUNK_DIMENSION; //The size of the data passed into marching cubes/transvoxel algorithm to get a fully connected and seamless chunk @@ -39,16 +44,23 @@ public class ChunkData { */ int worldZ; + /** + * The stride of the data + */ + int stride; + /** * Creates a chunk data * @param worldX The word x coordinate * @param worldY The word y coordinate * @param worldZ The word z coordinate + * @param stride The stride of the data */ - public ChunkData(int worldX, int worldY, int worldZ){ + public ChunkData(int worldX, int worldY, int worldZ, int stride){ this.worldX = worldX; this.worldY = worldY; this.worldZ = worldZ; + this.stride = stride; } @@ -211,5 +223,13 @@ public class ChunkData { return new Vector3i(worldX,worldY,worldZ); } + /** + * Gets the stride of the data + * @return The stride of the data + */ + public int getStride(){ + return stride; + } + } diff --git a/src/main/java/electrosphere/client/terrain/cache/ClientTerrainCache.java b/src/main/java/electrosphere/client/terrain/cache/ClientTerrainCache.java index 90f1dcf4..589f0952 100644 --- a/src/main/java/electrosphere/client/terrain/cache/ClientTerrainCache.java +++ b/src/main/java/electrosphere/client/terrain/cache/ClientTerrainCache.java @@ -5,6 +5,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Semaphore; import org.joml.Vector3i; @@ -16,19 +17,50 @@ import io.github.studiorailgun.HashUtils; */ public class ClientTerrainCache { - //cache capacity + /** + * Cache capacity + */ int cacheSize; - //the map of chunk key -> chunk data - Map cacheMap = new ConcurrentHashMap(); - //the list of keys in the cache + + /** + * The map of full res chunk key -> chunk data + */ + Map cacheMapFullRes = new ConcurrentHashMap(); + + /** + * The map of half res chunk key -> chunk data + */ + Map cacheMapHalfRes = new ConcurrentHashMap(); + + /** + * The map of quarter res chunk key -> chunk data + */ + Map cacheMapQuarterRes = new ConcurrentHashMap(); + + /** + * The map of eighth res chunk key -> chunk data + */ + Map cacheMapEighthRes = new ConcurrentHashMap(); + + /** + * The map of sixteenth res chunk key -> chunk data + */ + Map cacheMapSixteenthRes = new ConcurrentHashMap(); + + /** + * The list of keys in the cache + */ List cacheList = new CopyOnWriteArrayList(); - //A map of chunk to its world position + + /** + * A map of chunk to its world position + */ Map chunkPositionMap = new ConcurrentHashMap(); /** - * The map tracking chunks that have been requested + * The lock on the terrain cache */ - Map requestedChunks = new ConcurrentHashMap(); + Semaphore lock = new Semaphore(1); /** * Constructor @@ -46,23 +78,27 @@ public class ClientTerrainCache { * @param chunkData The chunk data to add at the specified positions */ public void addChunkDataToCache(int worldX, int worldY, int worldZ, ChunkData chunkData){ - cacheMap.put(getKey(worldX,worldY,worldZ),chunkData); + lock.acquireUninterruptibly(); + Map cache = this.getCache(chunkData.getStride()); + cache.put(getKey(worldX,worldY,worldZ),chunkData); chunkPositionMap.put(chunkData,new Vector3i(worldX,worldY,worldZ)); while(cacheList.size() > cacheSize){ Long currentChunk = cacheList.remove(0); - ChunkData data = cacheMap.remove(currentChunk); - Vector3i worldPos = data.getWorldPos(); - requestedChunks.remove(getKey(worldPos.x,worldPos.y,worldPos.z)); + cache.remove(currentChunk); } + lock.release(); } /** * Evicts all chunks from the cache */ public void evictAll(){ + lock.acquireUninterruptibly(); this.cacheList.clear(); - this.cacheMap.clear(); + this.cacheMapFullRes.clear(); + this.cacheMapHalfRes.clear(); this.chunkPositionMap.clear(); + lock.release(); } @@ -82,10 +118,14 @@ public class ClientTerrainCache { * @param worldX The x world position * @param worldY The y world position * @param worldZ The z world position + * @param stride The stride of the data * @return True if the cache contains chunk data at the specified point, false otherwise */ - public boolean containsChunkDataAtWorldPoint(int worldX, int worldY, int worldZ){ - return cacheMap.containsKey(getKey(worldX,worldY,worldZ)); + public boolean containsChunkDataAtWorldPoint(int worldX, int worldY, int worldZ, int stride){ + lock.acquireUninterruptibly(); + boolean rVal = this.getCache(stride).containsKey(getKey(worldX,worldY,worldZ)); + lock.release(); + return rVal; } @@ -97,10 +137,14 @@ public class ClientTerrainCache { * @param worldX The x world position * @param worldY The y world position * @param worldZ The z world position + * @param stride The stride of the data * @return The chunk data if it exists, null otherwise */ - public ChunkData getSubChunkDataAtPoint(int worldX, int worldY, int worldZ){ - return cacheMap.get(getKey(worldX,worldY,worldZ)); + public ChunkData getSubChunkDataAtPoint(int worldX, int worldY, int worldZ, int stride){ + lock.acquireUninterruptibly(); + ChunkData rVal = this.getCache(stride).get(getKey(worldX,worldY,worldZ)); + lock.release(); + return rVal; } /** @@ -108,7 +152,7 @@ public class ClientTerrainCache { * @return The list of all chunks in the cache */ public Collection getAllChunks(){ - return this.cacheMap.values(); + return this.cacheMapFullRes.values(); } /** @@ -119,34 +163,33 @@ public class ClientTerrainCache { public Vector3i getChunkPosition(ChunkData chunk){ return chunkPositionMap.get(chunk); } - - /** - * Gets the number of cells that have been requested - * @return The number of cells that have been requested - */ - public int getRequestedCellCount(){ - return this.requestedChunks.size(); - } - - /** - * Checks if a chunk has been requested or not - * @param worldX The x coordinate in the world - * @param worldY The y coordinate in the world - * @param worldZ The z coordinate in the world - * @return true if it has been requested, false otherwise - */ - public boolean hasRequested(int worldX, int worldY, int worldZ){ - return this.requestedChunks.containsKey(getKey(worldX, worldY, worldZ)); - } - - /** - * Marks a chunk as requested - * @param worldX The x coordinate in the world - * @param worldY The y coordinate in the world - * @param worldZ The z coordinate in the world - */ - public void markAsRequested(int worldX, int worldY, int worldZ){ - this.requestedChunks.put(getKey(worldX, worldY, worldZ),true); - } + /** + * Gets the cache + * @param stride The stride of the data + * @return The cache to use + */ + public Map getCache(int stride){ + switch(stride){ + case 0: { + return cacheMapFullRes; + } + case 1: { + return cacheMapHalfRes; + } + case 2: { + return cacheMapQuarterRes; + } + case 3: { + return cacheMapEighthRes; + } + case 4: { + return cacheMapEighthRes; + } + default: { + throw new Error("Invalid stride probided! " + stride); + } + } + } + } diff --git a/src/main/java/electrosphere/client/terrain/cells/ClientDrawCellManager.java b/src/main/java/electrosphere/client/terrain/cells/ClientDrawCellManager.java index 92ab7eb1..b84dd2c6 100644 --- a/src/main/java/electrosphere/client/terrain/cells/ClientDrawCellManager.java +++ b/src/main/java/electrosphere/client/terrain/cells/ClientDrawCellManager.java @@ -25,6 +25,11 @@ public class ClientDrawCellManager { */ static final int UPDATE_ATTEMPTS_PER_FRAME = 3; + /** + * The number of generation attempts before a cell is marked as having not requested its data + */ + static final int FAILED_GENERATION_ATTEMPT_THRESHOLD = 250; + /** * The distance to draw at full resolution */ @@ -35,6 +40,51 @@ public class ClientDrawCellManager { */ public static final double HALF_RES_DIST = 16 * ServerTerrainChunk.CHUNK_DIMENSION; + /** + * The distance for quarter resolution + */ + public static final double QUARTER_RES_DIST = 20 * ServerTerrainChunk.CHUNK_DIMENSION; + + /** + * The distance for eighth resolution + */ + public static final double EIGHTH_RES_DIST = 32 * ServerTerrainChunk.CHUNK_DIMENSION; + + /** + * The distance for sixteenth resolution + */ + public static final double SIXTEENTH_RES_DIST = 128 * ServerTerrainChunk.CHUNK_DIMENSION; + + /** + * Lod value for a full res chunk + */ + public static final int FULL_RES_LOD = 0; + + /** + * Lod value for a half res chunk + */ + public static final int HALF_RES_LOD = 1; + + /** + * Lod value for a quarter res chunk + */ + public static final int QUARTER_RES_LOD = 2; + + /** + * Lod value for a eighth res chunk + */ + public static final int EIGHTH_RES_LOD = 3; + + /** + * Lod value for a sixteenth res chunk + */ + public static final int SIXTEENTH_RES_LOD = 4; + + /** + * Lod value for evaluating all lod levels + */ + public static final int ALL_RES_LOD = 5; + /** * The octree holding all the chunks to evaluate */ @@ -80,6 +130,11 @@ public class ClientDrawCellManager { */ int generated = 0; + /** + * Tracks whether the cell manager has initialized or not + */ + boolean initialized = false; + /** * Constructor * @param voxelTextureAtlas The voxel texture atlas @@ -100,12 +155,24 @@ public class ClientDrawCellManager { Vector3d playerPos = EntityUtils.getPosition(Globals.playerEntity); //the sets to iterate through updatedLastFrame = true; - int attempts = 0; validCellCount = 0; - while(updatedLastFrame && attempts < UPDATE_ATTEMPTS_PER_FRAME){ - FloatingChunkTreeNode rootNode = this.chunkTree.getRoot(); - updatedLastFrame = this.recursivelyUpdateCells(rootNode, playerPos); - attempts++; + //update all full res cells + FloatingChunkTreeNode rootNode = this.chunkTree.getRoot(); + updatedLastFrame = this.recursivelyUpdateCells(rootNode, playerPos, HALF_RES_LOD); + if(!updatedLastFrame && !this.initialized){ + this.initialized = true; + } + if(!updatedLastFrame){ + updatedLastFrame = this.recursivelyUpdateCells(rootNode, playerPos, QUARTER_RES_LOD); + } + if(!updatedLastFrame){ + updatedLastFrame = this.recursivelyUpdateCells(rootNode, playerPos, EIGHTH_RES_LOD); + } + if(!updatedLastFrame){ + updatedLastFrame = this.recursivelyUpdateCells(rootNode, playerPos, SIXTEENTH_RES_LOD); + } + if(!updatedLastFrame){ + updatedLastFrame = this.recursivelyUpdateCells(rootNode, playerPos, ALL_RES_LOD); } } Globals.profiler.endCpuSample(); @@ -115,9 +182,10 @@ public class ClientDrawCellManager { * Recursively update child nodes * @param node The root node * @param playerPos The player's position + * @param minLeafLod The minimum LOD required to evaluate a leaf * @return true if there is work remaining to be done, false otherwise */ - private boolean recursivelyUpdateCells(FloatingChunkTreeNode node, Vector3d playerPos){ + private boolean recursivelyUpdateCells(FloatingChunkTreeNode node, Vector3d playerPos, int minLeafLod){ Vector3d playerRealPos = EntityUtils.getPosition(Globals.playerEntity); boolean updated = false; if(this.shouldSplit(playerPos, node)){ @@ -161,19 +229,34 @@ public class ClientDrawCellManager { Globals.profiler.endCpuSample(); updated = true; - } else if(shouldRequest(playerPos, node)){ + } else if(shouldRequest(playerPos, node, minLeafLod)){ Globals.profiler.beginCpuSample("ClientDrawCellManager.request"); + + //calculate what to request DrawCell cell = node.getData(); - this.requestChunks(node); - cell.setHasRequested(true); + List highResFaces = this.solveHighResFace(node); + + //actually send requests + if(this.requestChunks(node, highResFaces)){ + cell.setHasRequested(true); + } + Globals.profiler.endCpuSample(); updated = true; - } else if(shouldGenerate(playerPos, node)){ + } else if(shouldGenerate(playerPos, node, minLeafLod)){ Globals.profiler.beginCpuSample("ClientDrawCellManager.generate"); int lodLevel = this.getLODLevel(playerRealPos, node); List highResFaces = this.solveHighResFace(node); if(containsDataToGenerate(node,highResFaces)){ node.getData().generateDrawableEntity(textureAtlas, lodLevel, highResFaces); + if(node.getData().getFailedGenerationAttempts() > FAILED_GENERATION_ATTEMPT_THRESHOLD){ + node.getData().setHasRequested(false); + } + } else if(node.getData() != null){ + node.getData().setFailedGenerationAttempts(node.getData().getFailedGenerationAttempts() + 1); + if(node.getData().getFailedGenerationAttempts() > FAILED_GENERATION_ATTEMPT_THRESHOLD){ + node.getData().setHasRequested(false); + } } Globals.profiler.endCpuSample(); updated = true; @@ -181,7 +264,7 @@ public class ClientDrawCellManager { this.validCellCount++; List> children = new LinkedList>(node.getChildren()); for(FloatingChunkTreeNode child : children){ - boolean childUpdate = recursivelyUpdateCells(child, playerPos); + boolean childUpdate = recursivelyUpdateCells(child, playerPos, minLeafLod); if(childUpdate == true){ updated = true; } @@ -216,7 +299,22 @@ public class ClientDrawCellManager { node.canSplit() && ( ( - node.getLevel() < this.chunkTree.getMaxLevel() - 1 && + node.getLevel() < this.chunkTree.getMaxLevel() - SIXTEENTH_RES_LOD && + this.getMinDistance(pos, node) <= SIXTEENTH_RES_DIST + ) + || + ( + node.getLevel() < this.chunkTree.getMaxLevel() - EIGHTH_RES_LOD && + this.getMinDistance(pos, node) <= EIGHTH_RES_DIST + ) + || + ( + node.getLevel() < this.chunkTree.getMaxLevel() - QUARTER_RES_LOD && + this.getMinDistance(pos, node) <= QUARTER_RES_DIST + ) + || + ( + node.getLevel() < this.chunkTree.getMaxLevel() - HALF_RES_LOD && this.getMinDistance(pos, node) <= HALF_RES_DIST ) || @@ -355,13 +453,28 @@ public class ClientDrawCellManager { !node.isLeaf() && ( ( - node.getLevel() == this.chunkTree.getMaxLevel() - 1 && + node.getLevel() == this.chunkTree.getMaxLevel() - HALF_RES_LOD && this.getMinDistance(pos, node) > FULL_RES_DIST ) - || + || ( + node.getLevel() == this.chunkTree.getMaxLevel() - QUARTER_RES_LOD && this.getMinDistance(pos, node) > HALF_RES_DIST ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - EIGHTH_RES_LOD && + this.getMinDistance(pos, node) > QUARTER_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - SIXTEENTH_RES_LOD && + this.getMinDistance(pos, node) > EIGHTH_RES_DIST + ) + || + ( + this.getMinDistance(pos, node) > SIXTEENTH_RES_DIST + ) ) ; } @@ -370,13 +483,15 @@ public class ClientDrawCellManager { * Checks if this cell should request chunk data * @param pos the player's position * @param node the node + * @param minLeafLod The minimum LOD required to evaluate a leaf * @return true if should request chunk data, false otherwise */ - public boolean shouldRequest(Vector3d pos, FloatingChunkTreeNode node){ + public boolean shouldRequest(Vector3d pos, FloatingChunkTreeNode node, int minLeafLod){ return node.isLeaf() && node.getData() != null && !node.getData().hasRequested() && + (this.chunkTree.getMaxLevel() - node.getLevel()) < minLeafLod && ( ( node.getLevel() == this.chunkTree.getMaxLevel() @@ -385,9 +500,27 @@ public class ClientDrawCellManager { ) || ( - node.getLevel() == this.chunkTree.getMaxLevel() - 1 + node.getLevel() == this.chunkTree.getMaxLevel() - HALF_RES_LOD && - this.getMinDistance(pos, node) <= HALF_RES_DIST + this.getMinDistance(pos, node) <= QUARTER_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - QUARTER_RES_LOD + && + this.getMinDistance(pos, node) <= EIGHTH_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - EIGHTH_RES_LOD + && + this.getMinDistance(pos, node) <= EIGHTH_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - SIXTEENTH_RES_LOD + && + this.getMinDistance(pos, node) <= SIXTEENTH_RES_DIST ) ) ; @@ -397,24 +530,44 @@ public class ClientDrawCellManager { * Checks if this cell should generate * @param pos the player's position * @param node the node + * @param minLeafLod The minimum LOD required to evaluate a leaf * @return true if should generate, false otherwise */ - public boolean shouldGenerate(Vector3d pos, FloatingChunkTreeNode node){ + public boolean shouldGenerate(Vector3d pos, FloatingChunkTreeNode node, int minLeafLod){ return node.isLeaf() && node.getData() != null && !node.getData().hasGenerated() && + (this.chunkTree.getMaxLevel() - node.getLevel()) < minLeafLod && ( ( node.getLevel() == this.chunkTree.getMaxLevel() // && // this.getMinDistance(pos, node) <= FULL_RES_DIST ) - || + || ( - node.getLevel() == this.chunkTree.getMaxLevel() - 1 + node.getLevel() == this.chunkTree.getMaxLevel() - HALF_RES_LOD && - this.getMinDistance(pos, node) <= HALF_RES_DIST + this.getMinDistance(pos, node) <= QUARTER_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - QUARTER_RES_LOD + && + this.getMinDistance(pos, node) <= EIGHTH_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - EIGHTH_RES_LOD + && + this.getMinDistance(pos, node) <= EIGHTH_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - SIXTEENTH_RES_LOD + && + this.getMinDistance(pos, node) <= SIXTEENTH_RES_DIST ) ) ; @@ -490,31 +643,19 @@ public class ClientDrawCellManager { throw new Error("Unimplemented"); } - /** - * Checks if the terrain cache has a chunk at a given world point - * @param worldX the x coordinate - * @param worldY the y coordinate - * @param worldZ the z coordinate - * @return true if the chunk data exists, false otherwise - */ - boolean containsChunkDataAtWorldPoint(int worldX, int worldY, int worldZ){ - if(Globals.clientTerrainManager != null){ - return Globals.clientTerrainManager.containsChunkDataAtWorldPoint(worldX,worldY,worldZ); - } - return true; - } - /** * Requests all chunks for a given draw cell * @param cell The cell + * @return true if all cells were successfully requested, false otherwise */ - private void requestChunks(WorldOctTree.FloatingChunkTreeNode node){ + private boolean requestChunks(WorldOctTree.FloatingChunkTreeNode node, List highResFaces){ DrawCell cell = node.getData(); - int lodMultiplitier = this.chunkTree.getMaxLevel() - node.getLevel() + 1; - for(int i = 0; i < 2 * lodMultiplitier; i++){ - for(int j = 0; j < 2 * lodMultiplitier; j++){ - for(int k = 0; k < 2 * lodMultiplitier; k++){ - Vector3i posToCheck = new Vector3i(cell.getWorldPos()).add(i,j,k); + int lod = this.chunkTree.getMaxLevel() - node.getLevel(); + int spacingFactor = (int)Math.pow(2,lod); + for(int i = 0; i < 2; i++){ + for(int j = 0; j < 2; j++){ + for(int k = 0; k < 2; k++){ + Vector3i posToCheck = new Vector3i(cell.getWorldPos()).add(i*spacingFactor,j*spacingFactor,k*spacingFactor); if( posToCheck.x >= 0 && posToCheck.x < Globals.clientWorldData.getWorldDiscreteSize() && @@ -522,15 +663,65 @@ public class ClientDrawCellManager { posToCheck.y < Globals.clientWorldData.getWorldDiscreteSize() && posToCheck.z >= 0 && posToCheck.z < Globals.clientWorldData.getWorldDiscreteSize() && - !Globals.clientTerrainManager.containsChunkDataAtWorldPoint(posToCheck.x, posToCheck.y, posToCheck.z) + !Globals.clientTerrainManager.containsChunkDataAtWorldPoint(posToCheck.x, posToCheck.y, posToCheck.z, lod) ){ //client should request chunk data from server for each chunk necessary to create the model LoggerInterface.loggerNetworking.DEBUG("(Client) Send Request for terrain at " + posToCheck); - Globals.clientTerrainManager.requestChunk(posToCheck.x, posToCheck.y, posToCheck.z); + if(!Globals.clientTerrainManager.requestChunk(posToCheck.x, posToCheck.y, posToCheck.z, lod)){ + return false; + } } } } } + int highResLod = this.chunkTree.getMaxLevel() - (node.getLevel() + 1); + int highResSpacingFactor = (int)Math.pow(2,highResLod); + if(highResFaces != null){ + for(DrawCellFace highResFace : highResFaces){ + //x & y are in face-space + for(int x = 0; x < 3; x++){ + for(int y = 0; y < 3; y++){ + Vector3i posToCheck = null; + //implicitly performing transforms to adapt from face-space to world space + switch(highResFace){ + case X_POSITIVE: { + posToCheck = new Vector3i(cell.getWorldPos()).add(spacingFactor,x*highResSpacingFactor,y*highResSpacingFactor); + } break; + case X_NEGATIVE: { + posToCheck = new Vector3i(cell.getWorldPos()).add(0,x*highResSpacingFactor,y*highResSpacingFactor); + } break; + case Y_POSITIVE: { + posToCheck = new Vector3i(cell.getWorldPos()).add(x*highResSpacingFactor,spacingFactor,y*highResSpacingFactor); + } break; + case Y_NEGATIVE: { + posToCheck = new Vector3i(cell.getWorldPos()).add(x*highResSpacingFactor,0,y*highResSpacingFactor); + } break; + case Z_POSITIVE: { + posToCheck = new Vector3i(cell.getWorldPos()).add(x*highResSpacingFactor,y*highResSpacingFactor,spacingFactor); + } break; + case Z_NEGATIVE: { + posToCheck = new Vector3i(cell.getWorldPos()).add(x*highResSpacingFactor,y*highResSpacingFactor,0); + } break; + } + if( + posToCheck.x >= 0 && + posToCheck.x < Globals.clientWorldData.getWorldDiscreteSize() && + posToCheck.y >= 0 && + posToCheck.y < Globals.clientWorldData.getWorldDiscreteSize() && + posToCheck.z >= 0 && + posToCheck.z < Globals.clientWorldData.getWorldDiscreteSize() && + !Globals.clientTerrainManager.containsChunkDataAtWorldPoint(posToCheck.x, posToCheck.y, posToCheck.z, highResLod) + ){ + LoggerInterface.loggerNetworking.DEBUG("(Client) Send Request for terrain at " + posToCheck); + if(!Globals.clientTerrainManager.requestChunk(posToCheck.x, posToCheck.y, posToCheck.z, highResLod)){ + return false; + } + } + } + } + } + } + return true; } /** @@ -541,11 +732,12 @@ public class ClientDrawCellManager { */ private boolean containsDataToGenerate(WorldOctTree.FloatingChunkTreeNode node, List highResFaces){ DrawCell cell = node.getData(); - int lodMultiplitier = this.chunkTree.getMaxLevel() - node.getLevel() + 1; - for(int i = 0; i < 2 * lodMultiplitier; i++){ - for(int j = 0; j < 2 * lodMultiplitier; j++){ - for(int k = 0; k < 2 * lodMultiplitier; k++){ - Vector3i posToCheck = new Vector3i(cell.getWorldPos()).add(i,j,k); + int lod = this.chunkTree.getMaxLevel() - node.getLevel(); + int spacingFactor = (int)Math.pow(2,lod); + for(int i = 0; i < 2; i++){ + for(int j = 0; j < 2; j++){ + for(int k = 0; k < 2; k++){ + Vector3i posToCheck = new Vector3i(cell.getWorldPos()).add(i*spacingFactor,j*spacingFactor,k*spacingFactor); if( posToCheck.x >= 0 && posToCheck.x < Globals.clientWorldData.getWorldDiscreteSize() && @@ -553,38 +745,40 @@ public class ClientDrawCellManager { posToCheck.y < Globals.clientWorldData.getWorldDiscreteSize() && posToCheck.z >= 0 && posToCheck.z < Globals.clientWorldData.getWorldDiscreteSize() && - !Globals.clientTerrainManager.containsChunkDataAtWorldPoint(posToCheck.x, posToCheck.y, posToCheck.z) + !Globals.clientTerrainManager.containsChunkDataAtWorldPoint(posToCheck.x, posToCheck.y, posToCheck.z, lod) ){ return false; } } } } + int highResLod = this.chunkTree.getMaxLevel() - (node.getLevel() + 1); + int highResSpacingFactor = (int)Math.pow(2,highResLod); if(highResFaces != null){ for(DrawCellFace highResFace : highResFaces){ //x & y are in face-space - for(int x = 0; x < 2 * lodMultiplitier; x++){ - for(int y = 0; y < 2 * lodMultiplitier; y++){ + for(int x = 0; x < 3; x++){ + for(int y = 0; y < 3; y++){ Vector3i posToCheck = null; //implicitly performing transforms to adapt from face-space to world space switch(highResFace){ case X_POSITIVE: { - posToCheck = new Vector3i(cell.getWorldPos()).add(lodMultiplitier,x,y); + posToCheck = new Vector3i(cell.getWorldPos()).add(spacingFactor,x*highResSpacingFactor,y*highResSpacingFactor); } break; case X_NEGATIVE: { - posToCheck = new Vector3i(cell.getWorldPos()).add(-1,x,y); + posToCheck = new Vector3i(cell.getWorldPos()).add(0,x*highResSpacingFactor,y*highResSpacingFactor); } break; case Y_POSITIVE: { - posToCheck = new Vector3i(cell.getWorldPos()).add(x,lodMultiplitier,y); + posToCheck = new Vector3i(cell.getWorldPos()).add(x*highResSpacingFactor,spacingFactor,y*highResSpacingFactor); } break; case Y_NEGATIVE: { - posToCheck = new Vector3i(cell.getWorldPos()).add(x,-1,y); + posToCheck = new Vector3i(cell.getWorldPos()).add(x*highResSpacingFactor,0,y*highResSpacingFactor); } break; case Z_POSITIVE: { - posToCheck = new Vector3i(cell.getWorldPos()).add(x,y,lodMultiplitier); + posToCheck = new Vector3i(cell.getWorldPos()).add(x*highResSpacingFactor,y*highResSpacingFactor,spacingFactor); } break; case Z_NEGATIVE: { - posToCheck = new Vector3i(cell.getWorldPos()).add(x,y,-1); + posToCheck = new Vector3i(cell.getWorldPos()).add(x*highResSpacingFactor,y*highResSpacingFactor,0); } break; } if( @@ -594,7 +788,7 @@ public class ClientDrawCellManager { posToCheck.y < Globals.clientWorldData.getWorldDiscreteSize() && posToCheck.z >= 0 && posToCheck.z < Globals.clientWorldData.getWorldDiscreteSize() && - !Globals.clientTerrainManager.containsChunkDataAtWorldPoint(posToCheck.x, posToCheck.y, posToCheck.z) + !Globals.clientTerrainManager.containsChunkDataAtWorldPoint(posToCheck.x, posToCheck.y, posToCheck.z, highResLod) ){ return false; } @@ -685,6 +879,14 @@ public class ClientDrawCellManager { return generated; } + /** + * Gets whether the client draw cell manager has initialized or not + * @return true if it has initialized, false otherwise + */ + public boolean isInitialized(){ + return this.initialized; + } + } diff --git a/src/main/java/electrosphere/client/terrain/cells/DrawCell.java b/src/main/java/electrosphere/client/terrain/cells/DrawCell.java index 96eed5b9..d46a8719 100644 --- a/src/main/java/electrosphere/client/terrain/cells/DrawCell.java +++ b/src/main/java/electrosphere/client/terrain/cells/DrawCell.java @@ -87,6 +87,11 @@ public class DrawCell { * True if there are multiple types of voxels in this chunk */ boolean multiVoxelType = false; + + /** + * Number of failed generation attempts + */ + int failedGenerationAttempts = 0; /** @@ -116,7 +121,7 @@ public class DrawCell { boolean success = this.fillInData(lod); Globals.profiler.endCpuSample(); if(!success){ - this.setHasRequested(false); + this.setFailedGenerationAttempts(this.getFailedGenerationAttempts() + 1); return; } if(modelEntity != null){ @@ -129,7 +134,7 @@ public class DrawCell { success = this.fillInFaceData(chunkData,face,lod); Globals.profiler.endCpuSample(); if(!success){ - this.setHasRequested(false); + this.setFailedGenerationAttempts(this.getFailedGenerationAttempts() + 1); return; } } @@ -192,23 +197,39 @@ public class DrawCell { for(int y = 0; y < ChunkData.CHUNK_DATA_GENERATOR_SIZE; y++){ for(int z = 0; z < ChunkData.CHUNK_DATA_GENERATOR_SIZE; z++){ ChunkData currentChunk = Globals.clientTerrainManager.getChunkDataAtWorldPoint( - worldPos.x + (x * spacingFactor) / ChunkData.CHUNK_SIZE, - worldPos.y + (y * spacingFactor) / ChunkData.CHUNK_SIZE, - worldPos.z + (z * spacingFactor) / ChunkData.CHUNK_SIZE + worldPos.x + (x / ChunkData.CHUNK_SIZE) * spacingFactor, + worldPos.y + (y / ChunkData.CHUNK_SIZE) * spacingFactor, + worldPos.z + (z / ChunkData.CHUNK_SIZE) * spacingFactor, + lod ); if(currentChunk == null){ - return false; + Vector3i posToCheck = new Vector3i( + worldPos.x + (x / ChunkData.CHUNK_SIZE) * spacingFactor, + worldPos.y + (y / ChunkData.CHUNK_SIZE) * spacingFactor, + worldPos.z + (z / ChunkData.CHUNK_SIZE) * spacingFactor + ); + if( + posToCheck.x >= 0 && + posToCheck.x < Globals.clientWorldData.getWorldDiscreteSize() && + posToCheck.y >= 0 && + posToCheck.y < Globals.clientWorldData.getWorldDiscreteSize() && + posToCheck.z >= 0 && + posToCheck.z < Globals.clientWorldData.getWorldDiscreteSize() + ){ + return false; + } + } else { + weights[x][y][z] = currentChunk.getWeight( + x % ChunkData.CHUNK_SIZE, + y % ChunkData.CHUNK_SIZE, + z % ChunkData.CHUNK_SIZE + ); + types[x][y][z] = currentChunk.getType( + x % ChunkData.CHUNK_SIZE, + y % ChunkData.CHUNK_SIZE, + z % ChunkData.CHUNK_SIZE + ); } - weights[x][y][z] = currentChunk.getWeight( - (x * spacingFactor) % ChunkData.CHUNK_SIZE, - (y * spacingFactor) % ChunkData.CHUNK_SIZE, - (z * spacingFactor) % ChunkData.CHUNK_SIZE - ); - types[x][y][z] = currentChunk.getType( - (x * spacingFactor) % ChunkData.CHUNK_SIZE, - (y * spacingFactor) % ChunkData.CHUNK_SIZE, - (z * spacingFactor) % ChunkData.CHUNK_SIZE - ); //checks to see if there is only one type of voxel in this chunk if(!this.multiVoxelType){ @@ -233,26 +254,30 @@ public class DrawCell { */ private boolean fillInFaceData(TransvoxelChunkData chunkData, DrawCellFace higherLODFace, int lod){ int mainSpacing = (int)Math.pow(2,lod); - int higherResSpacing = (int)Math.pow(2,lod - 1); + int higherLOD = lod - 1; + int higherResSpacing = (int)Math.pow(2,higherLOD); float[][] faceWeights = new float[TransvoxelModelGeneration.FACE_DATA_DIMENSIONS][TransvoxelModelGeneration.FACE_DATA_DIMENSIONS]; int[][] faceTypes = new int[TransvoxelModelGeneration.FACE_DATA_DIMENSIONS][TransvoxelModelGeneration.FACE_DATA_DIMENSIONS]; //allocate face array for(int x = 0; x < TransvoxelModelGeneration.FACE_DATA_DIMENSIONS; x++){ for(int y = 0; y < TransvoxelModelGeneration.FACE_DATA_DIMENSIONS; y++){ - int worldCoordOffset1 = (x * higherResSpacing) / ChunkData.CHUNK_SIZE; - int worldCoordOffset2 = (y * higherResSpacing) / ChunkData.CHUNK_SIZE; + int worldCoordOffset1 = x / ChunkData.CHUNK_SIZE * higherResSpacing; + int worldCoordOffset2 = y / ChunkData.CHUNK_SIZE * higherResSpacing; //solve coordinates relative to the face - int localCoord1 = (x * higherResSpacing) % ChunkData.CHUNK_SIZE; - int localCoord2 = (y * higherResSpacing) % ChunkData.CHUNK_SIZE; + int localCoord1 = x % ChunkData.CHUNK_SIZE; + int localCoord2 = y % ChunkData.CHUNK_SIZE; //implicitly performing transforms to adapt from face-space to world & local space switch(higherLODFace){ case X_POSITIVE: { - ChunkData currentChunk = Globals.clientTerrainManager.getChunkDataAtWorldPoint(new Vector3i( - worldPos.x + (17 * mainSpacing) / ChunkData.CHUNK_SIZE, - worldPos.y + worldCoordOffset1, - worldPos.z + worldCoordOffset2 - )); + ChunkData currentChunk = Globals.clientTerrainManager.getChunkDataAtWorldPoint( + new Vector3i( + worldPos.x + mainSpacing, + worldPos.y + worldCoordOffset1, + worldPos.z + worldCoordOffset2 + ), + higherLOD + ); if(currentChunk == null){ return false; } @@ -268,11 +293,14 @@ public class DrawCell { ); } break; case X_NEGATIVE: { - ChunkData currentChunk = Globals.clientTerrainManager.getChunkDataAtWorldPoint(new Vector3i( - worldPos.x, - worldPos.y + worldCoordOffset1, - worldPos.z + worldCoordOffset2 - )); + ChunkData currentChunk = Globals.clientTerrainManager.getChunkDataAtWorldPoint( + new Vector3i( + worldPos.x, + worldPos.y + worldCoordOffset1, + worldPos.z + worldCoordOffset2 + ), + higherLOD + ); if(currentChunk == null){ return false; } @@ -288,11 +316,14 @@ public class DrawCell { ); } break; case Y_POSITIVE: { - ChunkData currentChunk = Globals.clientTerrainManager.getChunkDataAtWorldPoint(new Vector3i( - worldPos.x + worldCoordOffset1, - worldPos.y + (17 * mainSpacing) / ChunkData.CHUNK_SIZE, - worldPos.z + worldCoordOffset2 - )); + ChunkData currentChunk = Globals.clientTerrainManager.getChunkDataAtWorldPoint( + new Vector3i( + worldPos.x + worldCoordOffset1, + worldPos.y + mainSpacing, + worldPos.z + worldCoordOffset2 + ), + higherLOD + ); if(currentChunk == null){ return false; } @@ -308,11 +339,14 @@ public class DrawCell { ); } break; case Y_NEGATIVE: { - ChunkData currentChunk = Globals.clientTerrainManager.getChunkDataAtWorldPoint(new Vector3i( - worldPos.x + worldCoordOffset1, - worldPos.y, - worldPos.z + worldCoordOffset2 - )); + ChunkData currentChunk = Globals.clientTerrainManager.getChunkDataAtWorldPoint( + new Vector3i( + worldPos.x + worldCoordOffset1, + worldPos.y, + worldPos.z + worldCoordOffset2 + ), + higherLOD + ); if(currentChunk == null){ return false; } @@ -328,11 +362,14 @@ public class DrawCell { ); } break; case Z_POSITIVE: { - ChunkData currentChunk = Globals.clientTerrainManager.getChunkDataAtWorldPoint(new Vector3i( - worldPos.x + worldCoordOffset1, - worldPos.y + worldCoordOffset2, - worldPos.z + (17 * mainSpacing) / ChunkData.CHUNK_SIZE - )); + ChunkData currentChunk = Globals.clientTerrainManager.getChunkDataAtWorldPoint( + new Vector3i( + worldPos.x + worldCoordOffset1, + worldPos.y + worldCoordOffset2, + worldPos.z + mainSpacing + ), + higherLOD + ); if(currentChunk == null){ return false; } @@ -348,11 +385,14 @@ public class DrawCell { ); } break; case Z_NEGATIVE: { - ChunkData currentChunk = Globals.clientTerrainManager.getChunkDataAtWorldPoint(new Vector3i( - worldPos.x + worldCoordOffset1, - worldPos.y + worldCoordOffset2, - worldPos.z - )); + ChunkData currentChunk = Globals.clientTerrainManager.getChunkDataAtWorldPoint( + new Vector3i( + worldPos.x + worldCoordOffset1, + worldPos.y + worldCoordOffset2, + worldPos.z + ), + higherLOD + ); if(currentChunk == null){ return false; } @@ -443,6 +483,9 @@ public class DrawCell { */ public void setHasRequested(boolean hasRequested) { this.hasRequested = hasRequested; + if(!this.hasRequested){ + this.failedGenerationAttempts = 0; + } } /** @@ -469,6 +512,22 @@ public class DrawCell { return this.multiVoxelType; } + /** + * Gets the number of failed generation attempts + * @return The number of failed generation attempts + */ + public int getFailedGenerationAttempts(){ + return failedGenerationAttempts; + } + + /** + * Sets the number of failed generation attempts + * @param attempts The number of failed generation attempts + */ + public void setFailedGenerationAttempts(int attempts){ + this.failedGenerationAttempts = this.failedGenerationAttempts + attempts; + } + } diff --git a/src/main/java/electrosphere/client/terrain/cells/DrawCellManager.java b/src/main/java/electrosphere/client/terrain/cells/DrawCellManager.java index de4c287e..0d514141 100644 --- a/src/main/java/electrosphere/client/terrain/cells/DrawCellManager.java +++ b/src/main/java/electrosphere/client/terrain/cells/DrawCellManager.java @@ -176,7 +176,7 @@ public class DrawCellManager { posToCheck.y < Globals.clientWorldData.getWorldDiscreteSize() && posToCheck.z >= 0 && posToCheck.z < Globals.clientWorldData.getWorldDiscreteSize() && - !Globals.clientTerrainManager.containsChunkDataAtWorldPoint(posToCheck.x, posToCheck.y, posToCheck.z) + !Globals.clientTerrainManager.containsChunkDataAtWorldPoint(posToCheck.x, posToCheck.y, posToCheck.z, ChunkData.NO_STRIDE) ){ if(!requested.contains(requestKey)){ //client should request chunk data from server for each chunk necessary to create the model @@ -429,7 +429,7 @@ public class DrawCellManager { */ boolean containsChunkDataAtWorldPoint(int worldX, int worldY, int worldZ){ if(Globals.clientTerrainManager != null){ - return Globals.clientTerrainManager.containsChunkDataAtWorldPoint(worldX,worldY,worldZ); + return Globals.clientTerrainManager.containsChunkDataAtWorldPoint(worldX,worldY,worldZ,ChunkData.NO_STRIDE); } return true; } @@ -442,7 +442,7 @@ public class DrawCellManager { * @return The chunk data at the specified points */ ChunkData getChunkDataAtPoint(int worldX, int worldY, int worldZ){ - return Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldX,worldY,worldZ); + return Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldX,worldY,worldZ,ChunkData.NO_STRIDE); } diff --git a/src/main/java/electrosphere/client/terrain/manager/ClientTerrainManager.java b/src/main/java/electrosphere/client/terrain/manager/ClientTerrainManager.java index 5fdba3c1..a8e35170 100644 --- a/src/main/java/electrosphere/client/terrain/manager/ClientTerrainManager.java +++ b/src/main/java/electrosphere/client/terrain/manager/ClientTerrainManager.java @@ -6,7 +6,9 @@ import java.nio.IntBuffer; import java.util.Collection; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Semaphore; @@ -25,6 +27,7 @@ import electrosphere.renderer.meshgen.TerrainChunkModelGeneration; import electrosphere.renderer.model.Model; import electrosphere.server.terrain.manager.ServerTerrainChunk; import electrosphere.server.terrain.manager.ServerTerrainManager; +import io.github.studiorailgun.HashUtils; /** * Manages terrain storage and access on the client @@ -38,6 +41,16 @@ public class ClientTerrainManager { * Locks the terrain manager (eg if adding message from network) */ static Semaphore lock = new Semaphore(1); + + /** + * Maximum concurrent terrain requests + */ + public static final int MAX_CONCURRENT_REQUESTS = 500; + + /** + * Number of frames to wait before flagging a request as failed + */ + public static final int FAILED_REQUEST_THRESHOLD = 500; //The interpolation ratio of terrain public static final int INTERPOLATION_RATIO = ServerTerrainManager.SERVER_TERRAIN_MANAGER_INTERPOLATION_RATIO; @@ -58,6 +71,11 @@ public class ClientTerrainManager { //The queue of terrain chunk data to be buffered to gpu static List terrainChunkGenerationQueue = new CopyOnWriteArrayList(); + + /** + * Tracks what outgoing requests are currently active + */ + Map requestedMap = new ConcurrentHashMap(); /** * Constructor @@ -98,7 +116,7 @@ public class ClientTerrainManager { } } } - ChunkData data = new ChunkData(message.getworldX(), message.getworldY(), message.getworldZ()); + ChunkData data = new ChunkData(message.getworldX(), message.getworldY(), message.getworldZ(), ChunkData.NO_STRIDE); data.setVoxelType(values); data.setVoxelWeight(weights); terrainCache.addChunkDataToCache( @@ -106,6 +124,37 @@ public class ClientTerrainManager { data ); } break; + case SENDREDUCEDCHUNKDATA: { + int[][][] values = new int[ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE]; + float[][][] weights = new float[ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE]; + ByteBuffer buffer = ByteBuffer.wrap(message.getchunkData()); + FloatBuffer floatBuffer = buffer.asFloatBuffer(); + for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){ + for(int y = 0; y < ChunkData.CHUNK_SIZE; y++){ + for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){ + weights[x][y][z] = floatBuffer.get(); + } + } + } + IntBuffer intView = buffer.asIntBuffer(); + intView.position(floatBuffer.position()); + for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){ + for(int y = 0; y < ChunkData.CHUNK_SIZE; y++){ + for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){ + values[x][y][z] = intView.get(); + } + } + } + ChunkData data = new ChunkData(message.getworldX(), message.getworldY(), message.getworldZ(), message.getchunkResolution()); + data.setVoxelType(values); + data.setVoxelWeight(weights); + terrainCache.addChunkDataToCache( + message.getworldX(), message.getworldY(), message.getworldZ(), + data + ); + //remove from request map + this.requestedMap.remove(this.getRequestKey(message.getworldX(), message.getworldY(), message.getworldZ(), message.getchunkResolution())); + } break; default: LoggerInterface.loggerEngine.WARNING("ClientTerrainManager: unhandled network message of type" + message.getMessageSubtype()); break; @@ -114,6 +163,15 @@ public class ClientTerrainManager { for(TerrainMessage message : bouncedMessages){ messageQueue.add(message); } + //evaluate if any chunks have failed to request + for(Long key : this.requestedMap.keySet()){ + int duration = this.requestedMap.get(key); + if(duration > FAILED_REQUEST_THRESHOLD){ + this.requestedMap.remove(key); + } else { + this.requestedMap.put(key,duration + 1); + } + } lock.release(); Globals.profiler.endCpuSample(); } @@ -140,19 +198,21 @@ public class ClientTerrainManager { * @param worldX the x position * @param worldY the y position * @param worldZ the z position + * @param stride The stride of the data * @return true if the data exists, false otherwise */ - public boolean containsChunkDataAtWorldPoint(int worldX, int worldY, int worldZ){ - return terrainCache.containsChunkDataAtWorldPoint(worldX, worldY, worldZ); + public boolean containsChunkDataAtWorldPoint(int worldX, int worldY, int worldZ, int stride){ + return terrainCache.containsChunkDataAtWorldPoint(worldX, worldY, worldZ, stride); } /** * Checks if the terrain cache contains chunk data at a given world position * @param worldPos The vector containing the world-space position + * @param stride The stride of the data * @return true if the data exists, false otherwise */ - public boolean containsChunkDataAtWorldPoint(Vector3i worldPos){ - return terrainCache.containsChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z); + public boolean containsChunkDataAtWorldPoint(Vector3i worldPos, int stride){ + return this.containsChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, stride); } /** @@ -160,31 +220,24 @@ public class ClientTerrainManager { * @param worldX the world x coordinate of the chunk * @param worldY the world y coordinate of the chunk * @param worldZ the world z coordinate of the chunk + * @param stride The stride of the data + * @return true if the request was successfully sent, false otherwise */ - public void requestChunk(int worldX, int worldY, int worldZ){ - if(!this.terrainCache.hasRequested(worldX, worldY, worldZ)){ - Globals.clientConnection.queueOutgoingMessage(TerrainMessage.constructRequestChunkDataMessage( + public boolean requestChunk(int worldX, int worldY, int worldZ, int stride){ + boolean rVal = false; + lock.acquireUninterruptibly(); + if(this.requestedMap.size() < MAX_CONCURRENT_REQUESTS && !this.requestedMap.containsKey(this.getRequestKey(worldX, worldY, worldZ, stride))){ + Globals.clientConnection.queueOutgoingMessage(TerrainMessage.constructRequestReducedChunkDataMessage( worldX, worldY, - worldZ + worldZ, + stride )); + this.requestedMap.put(this.getRequestKey(worldX, worldY, worldZ, stride), 0); + rVal = true; } - } - - /** - * Checks that the cache contains chunk data at a real-space coordinate - * @param x the x coordinate - * @param y the y coordinate - * @param z the z coordinate - * @return true if the cache contains the chunk data at the coordinate, false otherwise - */ - public boolean containsChunkDataAtRealPoint(double x, double y, double z){ - assert clientWorldData != null; - return terrainCache.containsChunkDataAtWorldPoint( - clientWorldData.convertRealToChunkSpace(x), - clientWorldData.convertRealToChunkSpace(y), - clientWorldData.convertRealToChunkSpace(z) - ); + lock.release(); + return rVal; } /** @@ -192,19 +245,21 @@ public class ClientTerrainManager { * @param worldX The x component of the world coordinate * @param worldY The y component of the world coordinate * @param worldZ The z component of the world coordinate + * @param stride The stride of the data * @return The chunk data if it exists, otherwise null */ - public ChunkData getChunkDataAtWorldPoint(int worldX, int worldY, int worldZ){ - return terrainCache.getSubChunkDataAtPoint(worldX, worldY, worldZ); + public ChunkData getChunkDataAtWorldPoint(int worldX, int worldY, int worldZ, int stride){ + return terrainCache.getSubChunkDataAtPoint(worldX, worldY, worldZ, stride); } /** * Gets the chunk data at a given world position * @param worldPos The world position as a joml vector + * @param stride The stride of the data * @return The chunk data if it exists, otherwise null */ - public ChunkData getChunkDataAtWorldPoint(Vector3i worldPos){ - return terrainCache.getSubChunkDataAtPoint(worldPos.x, worldPos.y, worldPos.z); + public ChunkData getChunkDataAtWorldPoint(Vector3i worldPos, int stride){ + return this.getChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, stride); } @@ -258,11 +313,15 @@ public class ClientTerrainManager { } /** - * Gets the number of chunks that have been requested - * @return The number of chunks + * Gets the key for a given request + * @param worldX The world x coordinate + * @param worldY The world y coordinate + * @param worldZ The world z coordinate + * @param stride The stride of the data + * @return The key */ - public int getRequestedCellCount(){ - return this.terrainCache.getRequestedCellCount(); + private Long getRequestKey(int worldX, int worldY, int worldZ, int stride){ + return (long)HashUtils.cantorHash(worldY, worldZ, worldZ); } } diff --git a/src/main/java/electrosphere/client/terrain/sampling/ClientVoxelSampler.java b/src/main/java/electrosphere/client/terrain/sampling/ClientVoxelSampler.java index 8834bd06..d8fd6dab 100644 --- a/src/main/java/electrosphere/client/terrain/sampling/ClientVoxelSampler.java +++ b/src/main/java/electrosphere/client/terrain/sampling/ClientVoxelSampler.java @@ -36,8 +36,8 @@ public class ClientVoxelSampler { int voxelId = 0; Vector3i chunkSpacePos = Globals.clientWorldData.convertRealToWorldSpace(realPos); Vector3i voxelSpacePos = Globals.clientWorldData.convertRealToVoxelSpace(realPos); - if(Globals.clientTerrainManager.containsChunkDataAtWorldPoint(chunkSpacePos)){ - ChunkData chunkData = Globals.clientTerrainManager.getChunkDataAtWorldPoint(chunkSpacePos); + if(Globals.clientTerrainManager.containsChunkDataAtWorldPoint(chunkSpacePos, ChunkData.NO_STRIDE)){ + ChunkData chunkData = Globals.clientTerrainManager.getChunkDataAtWorldPoint(chunkSpacePos, ChunkData.NO_STRIDE); voxelId = chunkData.getType(voxelSpacePos); } else { return INVALID_POSITION; diff --git a/src/main/java/electrosphere/engine/Globals.java b/src/main/java/electrosphere/engine/Globals.java index 8f6a99d0..f6d1618e 100644 --- a/src/main/java/electrosphere/engine/Globals.java +++ b/src/main/java/electrosphere/engine/Globals.java @@ -4,6 +4,7 @@ import java.lang.management.ManagementFactory; import java.util.ArrayList; +import org.joml.Matrix4d; import org.joml.Matrix4f; import org.joml.Vector3f; @@ -257,7 +258,7 @@ public class Globals { //matrices for drawing models public static Matrix4f viewMatrix = new Matrix4f(); - public static Matrix4f projectionMatrix; + public static Matrix4d projectionMatrix; public static Matrix4f lightDepthMatrix = new Matrix4f(); //locations for shadow map specific variables diff --git a/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java b/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java index 9d6523d7..844c22da 100644 --- a/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java +++ b/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java @@ -245,7 +245,7 @@ public class ClientLoading { EntityCreationUtils.makeEntityDrawable(skybox, "Models/environment/skyboxSphere.fbx"); DrawableUtils.disableCulling(skybox); EntityUtils.getRotation(skybox).rotateX((float)(-Math.PI/2.0f)); - EntityUtils.getScale(skybox).mul(200000.0f); + EntityUtils.getScale(skybox).mul(600000.0f); Globals.assetManager.queueOverrideMeshShader("Models/environment/skyboxSphere.fbx", "Sphere", "Shaders/entities/skysphere/skysphere.vs", "Shaders/entities/skysphere/skysphere.fs"); //cloud ring pseudo skybox @@ -297,10 +297,14 @@ public class ClientLoading { Globals.clientSimulation.setLoadingTerrain(true); //wait for all the terrain data to arrive int i = 0; - while(blockForInit && Globals.clientDrawCellManager.updatedLastFrame() && Globals.threadManager.shouldKeepRunning()){ + while( + blockForInit && + !Globals.clientDrawCellManager.isInitialized() && + Globals.threadManager.shouldKeepRunning() + ){ i++; if(i % DRAW_CELL_UPDATE_RATE == 0){ - WindowUtils.updateLoadingWindow("WAITING ON SERVER TO SEND TERRAIN (" + Globals.clientTerrainManager.getAllChunks().size() + "/" + Globals.clientTerrainManager.getRequestedCellCount() + ")"); + WindowUtils.updateLoadingWindow("WAITING ON SERVER TO SEND TERRAIN (" + Globals.clientTerrainManager.getAllChunks().size() + ")"); } try { TimeUnit.MILLISECONDS.sleep(10); diff --git a/src/main/java/electrosphere/entity/types/terrain/TerrainChunkData.java b/src/main/java/electrosphere/entity/types/terrain/TerrainChunkData.java index e2982bed..b997e8d9 100644 --- a/src/main/java/electrosphere/entity/types/terrain/TerrainChunkData.java +++ b/src/main/java/electrosphere/entity/types/terrain/TerrainChunkData.java @@ -20,6 +20,11 @@ public class TerrainChunkData { //texture ratio vector List textureRatioVectors; //HOW MUCH of each texture in the atlas to sample + /** + * The LOD of the model + */ + int lod; + /** * Creates an object to hold data required to generate a chunk * @param vertices @@ -27,14 +32,16 @@ public class TerrainChunkData { * @param faceElements * @param uvs * @param textureSamplers + * @param lod The LOD of the model */ - public TerrainChunkData(List vertices, List normals, List faceElements, List uvs, List textureSamplers, List textureRatioVectors){ + public TerrainChunkData(List vertices, List normals, List faceElements, List uvs, List textureSamplers, List textureRatioVectors, int lod){ this.vertices = vertices; this.normals = normals; this.faceElements = faceElements; this.uvs = uvs; this.textureSamplers = textureSamplers; this.textureRatioVectors = textureRatioVectors; + this.lod = lod; } /** @@ -85,4 +92,12 @@ public class TerrainChunkData { return textureRatioVectors; } + /** + * Gets the LOD of the model + * @return The LOD + */ + public int getLOD(){ + return lod; + } + } diff --git a/src/main/java/electrosphere/logger/Logger.java b/src/main/java/electrosphere/logger/Logger.java index 30f8ccc0..f51b5f6f 100644 --- a/src/main/java/electrosphere/logger/Logger.java +++ b/src/main/java/electrosphere/logger/Logger.java @@ -130,6 +130,17 @@ public class Logger { } } + /** + * Logs an error message. + * This should be used every time we throw any kind of error in the engine + * @param e The exception to report + */ + public void ERROR(Error e){ + if(level == LogLevel.LOOP_DEBUG || level == LogLevel.DEBUG || level == LogLevel.INFO || level == LogLevel.WARNING || level == LogLevel.ERROR){ + e.printStackTrace(); + } + } + /** * Prints a message at the specified logging level * @param level The logging level diff --git a/src/main/java/electrosphere/net/client/protocol/TerrainProtocol.java b/src/main/java/electrosphere/net/client/protocol/TerrainProtocol.java index cdd606f2..45a1c6a2 100644 --- a/src/main/java/electrosphere/net/client/protocol/TerrainProtocol.java +++ b/src/main/java/electrosphere/net/client/protocol/TerrainProtocol.java @@ -47,6 +47,10 @@ public class TerrainProtocol implements ClientProtocolTemplate { LoggerInterface.loggerNetworking.DEBUG("(Client) Received terrain at " + message.getworldX() + " " + message.getworldY() + " " + message.getworldZ()); Globals.clientTerrainManager.attachTerrainMessage(message); } break; + case SENDREDUCEDCHUNKDATA: { + LoggerInterface.loggerNetworking.DEBUG("(Client) Received terrain at " + message.getworldX() + " " + message.getworldY() + " " + message.getworldZ() + " " + message.getchunkResolution()); + Globals.clientTerrainManager.attachTerrainMessage(message); + } break; case UPDATEVOXEL: { // //find what all drawcells might be updated by this voxel update @@ -79,8 +83,8 @@ public class TerrainProtocol implements ClientProtocolTemplate { } // //update the terrain cache - if(Globals.clientTerrainManager.containsChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z)){ - ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z); + if(Globals.clientTerrainManager.containsChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, ChunkData.NO_STRIDE)){ + ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, ChunkData.NO_STRIDE); if(data != null){ data.updatePosition( message.getvoxelX(), @@ -94,8 +98,8 @@ public class TerrainProtocol implements ClientProtocolTemplate { // //mark all relevant drawcells as updateable for(Vector3i worldPosToUpdate : positionsToUpdate){ - if(Globals.clientTerrainManager.containsChunkDataAtWorldPoint(worldPosToUpdate.x, worldPosToUpdate.y, worldPosToUpdate.z)){ - ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPosToUpdate.x, worldPosToUpdate.y, worldPosToUpdate.z); + if(Globals.clientTerrainManager.containsChunkDataAtWorldPoint(worldPosToUpdate.x, worldPosToUpdate.y, worldPosToUpdate.z, ChunkData.NO_STRIDE)){ + ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPosToUpdate.x, worldPosToUpdate.y, worldPosToUpdate.z, ChunkData.NO_STRIDE); if(data != null){ Globals.clientDrawCellManager.markUpdateable(worldPosToUpdate.x, worldPosToUpdate.y, worldPosToUpdate.z); } diff --git a/src/main/java/electrosphere/net/server/protocol/TerrainProtocol.java b/src/main/java/electrosphere/net/server/protocol/TerrainProtocol.java index 99b1d4f8..7c91d0e2 100644 --- a/src/main/java/electrosphere/net/server/protocol/TerrainProtocol.java +++ b/src/main/java/electrosphere/net/server/protocol/TerrainProtocol.java @@ -7,6 +7,7 @@ import java.util.function.Consumer; import org.joml.Vector3d; +import electrosphere.client.terrain.cache.ChunkData; import electrosphere.engine.Globals; import electrosphere.logger.LoggerInterface; import electrosphere.net.parser.net.message.TerrainMessage; @@ -32,6 +33,12 @@ public class TerrainProtocol implements ServerProtocolTemplate { ); return null; } + case REQUESTREDUCEDCHUNKDATA: { + sendWorldSubChunkAsyncStrided(connectionHandler, + message.getworldX(), message.getworldY(), message.getworldZ(), message.getchunkResolution() + ); + return null; + } default: { } break; } @@ -185,7 +192,65 @@ public class TerrainProtocol implements ServerProtocolTemplate { }; //request chunk - realm.getServerWorldData().getServerTerrainManager().getChunkAsync(worldX, worldY, worldZ, onLoad); + realm.getServerWorldData().getServerTerrainManager().getChunkAsync(worldX, worldY, worldZ, ChunkData.NO_STRIDE, onLoad); + + Globals.profiler.endCpuSample(); + } + + /** + * Sends a subchunk to the client + * @param connectionHandler The connection handler + * @param worldX the world x + * @param worldY the world y + * @param worldZ the world z + * @param stride The stride of the data + */ + static void sendWorldSubChunkAsyncStrided(ServerConnectionHandler connectionHandler, int worldX, int worldY, int worldZ, int stride){ + Globals.profiler.beginAggregateCpuSample("TerrainProtocol(server).sendWorldSubChunk"); + + // System.out.println("Received request for chunk " + message.getworldX() + " " + message.getworldY()); + Realm realm = Globals.playerManager.getPlayerRealm(connectionHandler.getPlayer()); + if(realm.getServerWorldData().getServerTerrainManager() == null){ + return; + } + + Consumer onLoad = (ServerTerrainChunk chunk) -> { + //The length along each access of the chunk data. Typically, should be at least 17. + //Because CHUNK_SIZE is 16, 17 adds the necessary extra value. Each chunk needs the value of the immediately following position to generate + //chunk data that connects seamlessly to the next chunk. + int xWidth = chunk.getWeights().length; + int yWidth = chunk.getWeights()[0].length; + int zWidth = chunk.getWeights()[0][0].length; + + ByteBuffer buffer = ByteBuffer.allocate(xWidth*yWidth*zWidth*(4+4)); + FloatBuffer floatView = buffer.asFloatBuffer(); + + for(int x = 0; x < xWidth; x++){ + for(int y = 0; y < yWidth; y++){ + for(int z = 0; z < zWidth; z++){ + floatView.put(chunk.getWeights()[x][y][z]); + } + } + } + + IntBuffer intView = buffer.asIntBuffer(); + intView.position(floatView.position()); + + for(int x = 0; x < xWidth; x++){ + for(int y = 0; y < yWidth; y++){ + for(int z = 0; z < zWidth; z++){ + intView.put(chunk.getValues()[x][y][z]); + } + } + } + + // System.out.println("(Server) Send terrain at " + worldX + " " + worldY + " " + worldZ); + LoggerInterface.loggerNetworking.DEBUG("(Server) Send terrain at " + worldX + " " + worldY + " " + worldZ); + connectionHandler.addMessagetoOutgoingQueue(TerrainMessage.constructSendReducedChunkDataMessage(worldX, worldY, worldZ, stride, buffer.array())); + }; + + //request chunk + realm.getServerWorldData().getServerTerrainManager().getChunkAsync(worldX, worldY, worldZ, stride, onLoad); Globals.profiler.endCpuSample(); } diff --git a/src/main/java/electrosphere/renderer/RenderPipelineState.java b/src/main/java/electrosphere/renderer/RenderPipelineState.java index caee8d89..60c0d83a 100644 --- a/src/main/java/electrosphere/renderer/RenderPipelineState.java +++ b/src/main/java/electrosphere/renderer/RenderPipelineState.java @@ -1,6 +1,7 @@ package electrosphere.renderer; import org.joml.FrustumIntersection; +import org.joml.Matrix4d; import org.joml.Matrix4f; import electrosphere.renderer.actor.instance.InstanceData; @@ -166,7 +167,7 @@ public class RenderPipelineState { * @param projectionMatrix the projection matrix * @param viewMatrix the view matrix */ - public void updateFrustumIntersection(Matrix4f projectionMatrix, Matrix4f viewMatrix){ + public void updateFrustumIntersection(Matrix4d projectionMatrix, Matrix4f viewMatrix){ Matrix4f projectionViewMatrix = new Matrix4f(); projectionViewMatrix.set(projectionMatrix); projectionViewMatrix.mul(viewMatrix); diff --git a/src/main/java/electrosphere/renderer/RenderingEngine.java b/src/main/java/electrosphere/renderer/RenderingEngine.java index 5e23ecdc..3b781cf2 100644 --- a/src/main/java/electrosphere/renderer/RenderingEngine.java +++ b/src/main/java/electrosphere/renderer/RenderingEngine.java @@ -453,7 +453,7 @@ public class RenderingEngine { // // Projection and View matrix creation // - Globals.projectionMatrix = new Matrix4f(); + Globals.projectionMatrix = new Matrix4d(); Globals.viewMatrix = new Matrix4f(); verticalFOV = (float)(Globals.verticalFOV * Math.PI /180.0f); //set local aspect ratio and global aspect ratio at the same time diff --git a/src/main/java/electrosphere/renderer/meshgen/TerrainChunkModelGeneration.java b/src/main/java/electrosphere/renderer/meshgen/TerrainChunkModelGeneration.java index ced73d9f..573be002 100644 --- a/src/main/java/electrosphere/renderer/meshgen/TerrainChunkModelGeneration.java +++ b/src/main/java/electrosphere/renderer/meshgen/TerrainChunkModelGeneration.java @@ -763,7 +763,7 @@ public class TerrainChunkModelGeneration { } //List vertices, List normals, List faceElements, List uvs - TerrainChunkData rVal = new TerrainChunkData(vertsFlat, normalsFlat, elementsFlat, UVs, textureSamplers, textureRatioData); + TerrainChunkData rVal = new TerrainChunkData(vertsFlat, normalsFlat, elementsFlat, UVs, textureSamplers, textureRatioData, 0); return rVal; } @@ -892,14 +892,15 @@ public class TerrainChunkModelGeneration { //bounding sphere logic + int distance = ServerTerrainChunk.CHUNK_DIMENSION / 2 * (int)Math.pow(2,data.getLOD()); mesh.updateBoundingSphere( - ServerTerrainChunk.CHUNK_DIMENSION, - ServerTerrainChunk.CHUNK_DIMENSION, - ServerTerrainChunk.CHUNK_DIMENSION, + distance, + distance, + distance, (float)Math.sqrt( - ServerTerrainChunk.CHUNK_DIMENSION * ServerTerrainChunk.CHUNK_DIMENSION + - ServerTerrainChunk.CHUNK_DIMENSION * ServerTerrainChunk.CHUNK_DIMENSION + - ServerTerrainChunk.CHUNK_DIMENSION * ServerTerrainChunk.CHUNK_DIMENSION + distance * distance + + distance * distance + + distance * distance )); diff --git a/src/main/java/electrosphere/renderer/meshgen/TransvoxelModelGeneration.java b/src/main/java/electrosphere/renderer/meshgen/TransvoxelModelGeneration.java index 76dd24b1..65356f42 100644 --- a/src/main/java/electrosphere/renderer/meshgen/TransvoxelModelGeneration.java +++ b/src/main/java/electrosphere/renderer/meshgen/TransvoxelModelGeneration.java @@ -1335,7 +1335,7 @@ public class TransvoxelModelGeneration { chunkData.yNegativeEdgeAtlas[(x+0)*2+0][(z+0)*2+0], chunkData.yNegativeEdgeAtlas[(x+1)*2+0][(z+0)*2+0], chunkData.yNegativeEdgeAtlas[(x+0)*2+0][(z+1)*2+0], chunkData.yNegativeEdgeAtlas[(x+1)*2+0][(z+1)*2+0] ); - polygonizeTransition(currentTransitionCell, TerrainChunkModelGeneration.MIN_ISO_VALUE, triangles, samplerTriangles, vertMap, verts, normals, trianglesSharingVert, false); + polygonizeTransition(currentTransitionCell, TerrainChunkModelGeneration.MIN_ISO_VALUE, triangles, samplerTriangles, vertMap, verts, normals, trianglesSharingVert, true); // //Generate the normal cell with half width @@ -1349,7 +1349,7 @@ public class TransvoxelModelGeneration { chunkData.yNegativeEdgeAtlas[(x+0)*2+0][(z+0)*2+0], chunkData.yNegativeEdgeAtlas[(x+0)*2+0][(z+1)*2+0], chunkData.yNegativeEdgeAtlas[(x+1)*2+0][(z+1)*2+0], chunkData.yNegativeEdgeAtlas[(x+1)*2+0][(z+0)*2+0] ); //polygonize the current gridcell - polygonize(currentCell, TerrainChunkModelGeneration.MIN_ISO_VALUE, triangles, samplerTriangles, vertMap, verts, normals, trianglesSharingVert, false); + polygonize(currentCell, TerrainChunkModelGeneration.MIN_ISO_VALUE, triangles, samplerTriangles, vertMap, verts, normals, trianglesSharingVert, true); } } } else { @@ -1694,7 +1694,7 @@ public class TransvoxelModelGeneration { } //List vertices, List normals, List faceElements, List uvs - TerrainChunkData rVal = new TerrainChunkData(vertsFlat, normalsFlat, elementsFlat, UVs, textureSamplers, textureRatioData); + TerrainChunkData rVal = new TerrainChunkData(vertsFlat, normalsFlat, elementsFlat, UVs, textureSamplers, textureRatioData, chunkData.levelOfDetail); return rVal; } diff --git a/src/main/java/electrosphere/server/terrain/generation/DefaultChunkGenerator.java b/src/main/java/electrosphere/server/terrain/generation/DefaultChunkGenerator.java index c3c2518b..2a5f5b5a 100644 --- a/src/main/java/electrosphere/server/terrain/generation/DefaultChunkGenerator.java +++ b/src/main/java/electrosphere/server/terrain/generation/DefaultChunkGenerator.java @@ -22,7 +22,7 @@ public class DefaultChunkGenerator implements ChunkGenerator { } @Override - public ServerTerrainChunk generateChunk(int worldX, int worldY, int worldZ) { + public ServerTerrainChunk generateChunk(int worldX, int worldY, int worldZ, int stride) { //Each chunk also needs custody of the next chunk's first values so that they can perfectly overlap. //Hence, width should actually be chunk dimension + 1 float[][][] weights = new float[ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION]; diff --git a/src/main/java/electrosphere/server/terrain/generation/OverworldChunkGenerator.java b/src/main/java/electrosphere/server/terrain/generation/OverworldChunkGenerator.java index 15bd5d21..6674257a 100644 --- a/src/main/java/electrosphere/server/terrain/generation/OverworldChunkGenerator.java +++ b/src/main/java/electrosphere/server/terrain/generation/OverworldChunkGenerator.java @@ -28,7 +28,7 @@ public class OverworldChunkGenerator implements ChunkGenerator { } @Override - public ServerTerrainChunk generateChunk(int worldX, int worldY, int worldZ) { + public ServerTerrainChunk generateChunk(int worldX, int worldY, int worldZ, int stride) { ServerTerrainChunk returnedChunk; //Each chunk also needs custody of the next chunk's first values so that they can perfectly overlap. //Hence, width should actually be chunk dimension + 1 diff --git a/src/main/java/electrosphere/server/terrain/generation/TestGenerationChunkGenerator.java b/src/main/java/electrosphere/server/terrain/generation/TestGenerationChunkGenerator.java index 669c6fb1..16d83fce 100644 --- a/src/main/java/electrosphere/server/terrain/generation/TestGenerationChunkGenerator.java +++ b/src/main/java/electrosphere/server/terrain/generation/TestGenerationChunkGenerator.java @@ -1,6 +1,5 @@ package electrosphere.server.terrain.generation; -import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -24,7 +23,7 @@ public class TestGenerationChunkGenerator implements ChunkGenerator { /** * The size of the realm for testing generation */ - public static final int GENERATOR_REALM_SIZE = 32; + public static final int GENERATOR_REALM_SIZE = 512; /** * The default biome index @@ -65,36 +64,14 @@ public class TestGenerationChunkGenerator implements ChunkGenerator { } @Override - public ServerTerrainChunk generateChunk(int worldX, int worldY, int worldZ) { + public ServerTerrainChunk generateChunk(int worldX, int worldY, int worldZ, int stride) { Globals.profiler.beginAggregateCpuSample("TestGenerationChunkGenerator.generateChunk"); ServerTerrainChunk rVal = null; - float[][][] weights; - int[][][] values; + float[][][] weights = new float[ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION];; + int[][][] values = new int[ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION]; - if(worldX == 0 || worldZ == 0){ - //generate flat ground for the player to spawn on - weights = new float[ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION]; - values = new int[ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION]; - for(int x = 0; x < ServerTerrainChunk.CHUNK_DIMENSION; x++){ - for(int y = 0; y < ServerTerrainChunk.CHUNK_DIMENSION; y++){ - Arrays.fill(weights[x][y],-1f); - } - } - if(worldY == 0){ - for(int x = 0; x < ServerTerrainChunk.CHUNK_DIMENSION; x++){ - for(int z = 0; z < ServerTerrainChunk.CHUNK_DIMENSION; z++){ - values[x][0][z] = 1; - weights[x][0][z] = 0.1f; - } - } - } - - - - } else { + try { //actual generation algo - weights = new float[ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION]; - values = new int[ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION]; //biome of the current chunk BiomeData surfaceBiome = this.terrainModel.getSurfaceBiome(worldX, worldY, worldZ); @@ -105,11 +82,22 @@ public class TestGenerationChunkGenerator implements ChunkGenerator { throw new Error("Undefined heightmap generator in biome! " + surfaceBiome.getId() + " " + surfaceBiome.getDisplayName() + " " + surfaceParams.getSurfaceGenTag()); } + //stride value + int strideValue = (int)Math.pow(2,stride); + //presolve heightfield float[][] heightfield = new float[ServerTerrainChunk.CHUNK_DIMENSION][ServerTerrainChunk.CHUNK_DIMENSION]; for(int x = 0; x < ServerTerrainChunk.CHUNK_DIMENSION; x++){ for(int z = 0; z < ServerTerrainChunk.CHUNK_DIMENSION; z++){ - heightfield[x][z] = heightmapGen.getHeight(this.terrainModel.getSeed(), this.serverWorldData.convertVoxelToRealSpace(x, worldX), this.serverWorldData.convertVoxelToRealSpace(z, worldZ)); + int finalWorldX = worldX + ((x * strideValue) / ServerTerrainChunk.CHUNK_DIMENSION); + int finalWorldZ = worldZ + ((z * strideValue) / ServerTerrainChunk.CHUNK_DIMENSION); + int finalChunkX = (x * strideValue) % ServerTerrainChunk.CHUNK_DIMENSION; + int finalChunkZ = (z * strideValue) % ServerTerrainChunk.CHUNK_DIMENSION; + heightfield[x][z] = heightmapGen.getHeight( + this.terrainModel.getSeed(), + this.serverWorldData.convertVoxelToRealSpace(finalChunkX, finalWorldX), + this.serverWorldData.convertVoxelToRealSpace(finalChunkZ, finalWorldZ) + ); } } @@ -117,15 +105,22 @@ public class TestGenerationChunkGenerator implements ChunkGenerator { Globals.profiler.beginAggregateCpuSample("TestGenerationChunkGenerator - Generate slice"); for(int y = 0; y < ServerTerrainChunk.CHUNK_DIMENSION; y++){ for(int z = 0; z < ServerTerrainChunk.CHUNK_DIMENSION; z++){ - GeneratedVoxel voxel = this.getVoxel(worldX, worldY, worldZ, x, y, z, heightfield, this.terrainModel, surfaceBiome); + int finalWorldX = worldX + ((x * strideValue) / ServerTerrainChunk.CHUNK_DIMENSION); + int finalWorldY = worldY + ((y * strideValue) / ServerTerrainChunk.CHUNK_DIMENSION); + int finalWorldZ = worldZ + ((z * strideValue) / ServerTerrainChunk.CHUNK_DIMENSION); + int finalChunkX = (x * strideValue) % ServerTerrainChunk.CHUNK_DIMENSION; + int finalChunkY = (y * strideValue) % ServerTerrainChunk.CHUNK_DIMENSION; + int finalChunkZ = (z * strideValue) % ServerTerrainChunk.CHUNK_DIMENSION; + GeneratedVoxel voxel = this.getVoxel(finalWorldX, finalWorldY, finalWorldZ, finalChunkX, finalChunkY, finalChunkZ, heightfield[x][z], this.terrainModel, surfaceBiome); weights[x][y][z] = voxel.weight; values[x][y][z] = voxel.type; } } Globals.profiler.endCpuSample(); } + } catch(Exception ex){ + ex.printStackTrace(); } - rVal = new ServerTerrainChunk(worldX, worldY, worldZ, weights, values); Globals.profiler.endCpuSample(); return rVal; @@ -144,7 +139,7 @@ public class TestGenerationChunkGenerator implements ChunkGenerator { * @param chunkX The chunk x pos * @param chunkY The chunk y pos * @param chunkZ The chunk z pos - * @param heightfield The precomputed heightfield + * @param surfaceHeight The height of the surface at x,z * @param terrainModel The terrain model * @param surfaceBiome The surface biome of the chunk * @return The value of the chunk @@ -152,7 +147,7 @@ public class TestGenerationChunkGenerator implements ChunkGenerator { private GeneratedVoxel getVoxel( int worldX, int worldY, int worldZ, int chunkX, int chunkY, int chunkZ, - float[][] heightfield, + float surfaceHeight, TerrainModel terrainModel, BiomeData surfaceBiome ){ @@ -163,16 +158,15 @@ public class TestGenerationChunkGenerator implements ChunkGenerator { throw new Error("Undefined heightmap generator in biome! " + surfaceBiome.getId() + " " + surfaceBiome.getDisplayName() + " " + surfaceParams.getSurfaceGenTag()); } - double realX = this.serverWorldData.convertVoxelToRealSpace(chunkY,worldX); + double realX = this.serverWorldData.convertVoxelToRealSpace(chunkX,worldX); double realY = this.serverWorldData.convertVoxelToRealSpace(chunkY,worldY); - double realZ = this.serverWorldData.convertVoxelToRealSpace(chunkY,worldZ); + double realZ = this.serverWorldData.convertVoxelToRealSpace(chunkZ,worldZ); - float surfaceHeight = heightfield[chunkX][chunkZ]; double flooredSurfaceHeight = Math.floor(surfaceHeight); Globals.profiler.endCpuSample(); if(realY < surfaceHeight - 1){ return getSubsurfaceVoxel( - worldX,worldY, worldZ, + worldX, worldY, worldZ, chunkX, chunkY, chunkZ, realX, realY, realZ, surfaceHeight, flooredSurfaceHeight, @@ -181,7 +175,7 @@ public class TestGenerationChunkGenerator implements ChunkGenerator { ); } else if(realY > flooredSurfaceHeight) { return getOverSurfaceVoxel( - worldX,worldY, worldZ, + worldX, worldY, worldZ, chunkX, chunkY, chunkZ, realX, realY, realZ, surfaceHeight, flooredSurfaceHeight, @@ -190,7 +184,7 @@ public class TestGenerationChunkGenerator implements ChunkGenerator { ); } else { return getSurfaceVoxel( - worldX,worldY, worldZ, + worldX, worldY, worldZ, chunkX, chunkY, chunkZ, realX, realY, realZ, surfaceHeight, flooredSurfaceHeight, diff --git a/src/main/java/electrosphere/server/terrain/generation/interfaces/ChunkGenerator.java b/src/main/java/electrosphere/server/terrain/generation/interfaces/ChunkGenerator.java index bb002835..d6e5afa3 100644 --- a/src/main/java/electrosphere/server/terrain/generation/interfaces/ChunkGenerator.java +++ b/src/main/java/electrosphere/server/terrain/generation/interfaces/ChunkGenerator.java @@ -13,9 +13,10 @@ public interface ChunkGenerator { * @param worldX The x component * @param worldY The y component * @param worldZ The z component + * @param stride The stride of the data * @return The chunk */ - public ServerTerrainChunk generateChunk(int worldX, int worldY, int worldZ); + public ServerTerrainChunk generateChunk(int worldX, int worldY, int worldZ, int stride); /** * Sets the terrain model for the generation algorithm diff --git a/src/main/java/electrosphere/server/terrain/manager/ChunkGenerationThread.java b/src/main/java/electrosphere/server/terrain/manager/ChunkGenerationThread.java index b66556d3..7d391c54 100644 --- a/src/main/java/electrosphere/server/terrain/manager/ChunkGenerationThread.java +++ b/src/main/java/electrosphere/server/terrain/manager/ChunkGenerationThread.java @@ -4,6 +4,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import electrosphere.engine.Globals; +import electrosphere.logger.LoggerInterface; import electrosphere.server.terrain.diskmap.ChunkDiskMap; import electrosphere.server.terrain.generation.interfaces.ChunkGenerator; @@ -51,6 +52,11 @@ public class ChunkGenerationThread implements Runnable { * The world z coordinate */ int worldZ; + + /** + * The stride of the data + */ + int stride; /** * The work to do once the chunk is available @@ -65,6 +71,7 @@ public class ChunkGenerationThread implements Runnable { * @param worldX The world x coordinate * @param worldY The world y coordinate * @param worldZ The world z coordinate + * @param stride The stride of the data * @param onLoad The work to do once the chunk is available */ public ChunkGenerationThread( @@ -72,6 +79,7 @@ public class ChunkGenerationThread implements Runnable { ServerChunkCache chunkCache, ChunkGenerator chunkGenerator, int worldX, int worldY, int worldZ, + int stride, Consumer onLoad ){ this.chunkDiskMap = chunkDiskMap; @@ -80,6 +88,7 @@ public class ChunkGenerationThread implements Runnable { this.worldX = worldX; this.worldY = worldY; this.worldZ = worldZ; + this.stride = stride; this.onLoad = onLoad; } @@ -87,37 +96,41 @@ public class ChunkGenerationThread implements Runnable { public void run() { ServerTerrainChunk chunk = null; int i = 0; - while(chunk == null && i < MAX_TIME_TO_WAIT && Globals.threadManager.shouldKeepRunning()){ - if(chunkCache.containsChunk(worldX,worldY,worldZ)){ - chunk = chunkCache.get(worldX,worldY,worldZ); - } else { - //pull from disk if it exists - if(chunkDiskMap != null){ - if(chunkDiskMap.containsTerrainAtPosition(worldX, worldY, worldZ)){ - chunk = chunkDiskMap.getTerrainChunk(worldX, worldY, worldZ); + try { + while(chunk == null && i < MAX_TIME_TO_WAIT && Globals.threadManager.shouldKeepRunning()){ + if(chunkCache.containsChunk(worldX, worldY, worldZ, stride)){ + chunk = chunkCache.get(worldX, worldY, worldZ, stride); + } else { + //pull from disk if it exists + if(chunkDiskMap != null){ + if(chunkDiskMap.containsTerrainAtPosition(worldX, worldY, worldZ)){ + chunk = chunkDiskMap.getTerrainChunk(worldX, worldY, worldZ); + } + } + //generate if it does not exist + if(chunk == null){ + chunk = chunkGenerator.generateChunk(worldX, worldY, worldZ, stride); + } + if(chunk != null){ + chunkCache.add(worldX, worldY, worldZ, stride, chunk); } } - //generate if it does not exist if(chunk == null){ - chunk = chunkGenerator.generateChunk(worldX, worldY, worldZ); - } - if(chunk != null){ - chunkCache.add(worldX, worldY, worldZ, chunk); + try { + TimeUnit.MILLISECONDS.sleep(WAIT_TIME_MS); + } catch (InterruptedException e) { + e.printStackTrace(); + } } + i++; } - if(chunk == null){ - try { - TimeUnit.MILLISECONDS.sleep(WAIT_TIME_MS); - } catch (InterruptedException e) { - e.printStackTrace(); - } + if(i >= MAX_TIME_TO_WAIT){ + throw new Error("Failed to resolve chunk!"); } - i++; + this.onLoad.accept(chunk); + } catch (Error e){ + LoggerInterface.loggerEngine.ERROR(e); } - if(i >= MAX_TIME_TO_WAIT){ - throw new Error("Failed to resolve chunk!"); - } - this.onLoad.accept(chunk); } } diff --git a/src/main/java/electrosphere/server/terrain/manager/ServerChunkCache.java b/src/main/java/electrosphere/server/terrain/manager/ServerChunkCache.java index 3d8fbc9b..da685ea8 100644 --- a/src/main/java/electrosphere/server/terrain/manager/ServerChunkCache.java +++ b/src/main/java/electrosphere/server/terrain/manager/ServerChunkCache.java @@ -6,8 +6,11 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Semaphore; +import io.github.studiorailgun.HashUtils; + /** * Caches chunk data on the server */ @@ -16,7 +19,7 @@ public class ServerChunkCache { /** * Number of chunks to cache */ - static final int CACHE_SIZE = 5000; + static final int CACHE_SIZE = 2500; /** * The size of the cache @@ -24,19 +27,39 @@ public class ServerChunkCache { int cacheSize = CACHE_SIZE; /** - * The cached data + * The map of full res chunk key -> chunk data */ - Map chunkCache = new HashMap(); + Map cacheMapFullRes = new ConcurrentHashMap(); + + /** + * The map of half res chunk key -> chunk data + */ + Map cacheMapHalfRes = new ConcurrentHashMap(); + + /** + * The map of quarter res chunk key -> chunk data + */ + Map cacheMapQuarterRes = new ConcurrentHashMap(); + + /** + * The map of eighth res chunk key -> chunk data + */ + Map cacheMapEighthRes = new ConcurrentHashMap(); + + /** + * The map of sixteenth res chunk key -> chunk data + */ + Map cacheMapSixteenthRes = new ConcurrentHashMap(); /** * Tracks how recently a chunk has been queries for (used for evicting old chunks from cache) */ - List queryRecencyQueue = new LinkedList(); + List queryRecencyQueue = new LinkedList(); /** * Tracks what chunks are already queued to be asynchronously loaded. Used so we don't have two threads generating/fetching the same chunk */ - Map queuedChunkMap = new HashMap(); + Map queuedChunkMap = new HashMap(); /** * The lock for thread safety @@ -49,7 +72,7 @@ public class ServerChunkCache { */ public Collection getContents(){ lock.acquireUninterruptibly(); - Collection rVal = Collections.unmodifiableCollection(chunkCache.values()); + Collection rVal = Collections.unmodifiableCollection(cacheMapFullRes.values()); lock.release(); return rVal; } @@ -59,7 +82,8 @@ public class ServerChunkCache { */ public void clear(){ lock.acquireUninterruptibly(); - chunkCache.clear(); + cacheMapFullRes.clear(); + cacheMapHalfRes.clear(); lock.release(); } @@ -68,15 +92,17 @@ public class ServerChunkCache { * @param worldX The world x coordinate * @param worldY The world y coordinate * @param worldZ The world z coordinate + * @param stride The stride of the data * @return The chunk */ - public ServerTerrainChunk get(int worldX, int worldY, int worldZ){ + public ServerTerrainChunk get(int worldX, int worldY, int worldZ, int stride){ ServerTerrainChunk rVal = null; - String key = this.getKey(worldX, worldY, worldZ); + Long key = this.getKey(worldX, worldY, worldZ); lock.acquireUninterruptibly(); queryRecencyQueue.remove(key); queryRecencyQueue.add(0, key); - rVal = this.chunkCache.get(key); + Map cache = this.getCache(stride); + rVal = cache.get(key); lock.release(); return rVal; } @@ -86,13 +112,15 @@ public class ServerChunkCache { * @param worldX The world x coordinate of the chunk * @param worldY The world y coordinate of the chunk * @param worldZ The world z coordinate of the chunk + * @param stride The stride of the data * @param chunk The chunk itself */ - public void add(int worldX, int worldY, int worldZ, ServerTerrainChunk chunk){ - String key = this.getKey(worldX, worldY, worldZ); + public void add(int worldX, int worldY, int worldZ, int stride, ServerTerrainChunk chunk){ + Long key = this.getKey(worldX, worldY, worldZ); lock.acquireUninterruptibly(); queryRecencyQueue.add(0, key); - this.chunkCache.put(key, chunk); + Map cache = this.getCache(stride); + cache.put(key, chunk); lock.release(); } @@ -101,12 +129,14 @@ public class ServerChunkCache { * @param worldX The world x coordinate * @param worldY The world y coordinate * @param worldZ The world z coordinate + * @param stride The stride of the data * @return true if the cache contains this chunk, false otherwise */ - public boolean containsChunk(int worldX, int worldY, int worldZ){ - String key = this.getKey(worldX,worldY,worldZ); + public boolean containsChunk(int worldX, int worldY, int worldZ, int stride){ + Long key = this.getKey(worldX,worldY,worldZ); lock.acquireUninterruptibly(); - boolean rVal = this.chunkCache.containsKey(key); + Map cache = this.getCache(stride); + boolean rVal = cache.containsKey(key); lock.release(); return rVal; } @@ -118,8 +148,8 @@ public class ServerChunkCache { * @param worldZ The z component * @return The key */ - public String getKey(int worldX, int worldY, int worldZ){ - return worldX + "_" + worldY + "_" + worldZ; + public long getKey(int worldX, int worldY, int worldZ){ + return HashUtils.cantorHash(worldX, worldY, worldZ); } /** @@ -130,7 +160,7 @@ public class ServerChunkCache { * @return true if the chunk is already queued, false otherwise */ public boolean chunkIsQueued(int worldX, int worldY, int worldZ){ - String key = this.getKey(worldX,worldY,worldZ); + Long key = this.getKey(worldX,worldY,worldZ); lock.acquireUninterruptibly(); boolean rVal = this.queuedChunkMap.containsKey(key); lock.release(); @@ -144,7 +174,7 @@ public class ServerChunkCache { * @param worldZ The world z position of the chunk */ public void queueChunk(int worldX, int worldY, int worldZ){ - String key = this.getKey(worldX,worldY,worldZ); + Long key = this.getKey(worldX,worldY,worldZ); lock.acquireUninterruptibly(); this.queuedChunkMap.put(key,true); lock.release(); @@ -155,12 +185,41 @@ public class ServerChunkCache { * @param worldX The world x position of the chunk * @param worldY The world y position of the chunk * @param worldZ The world z position of the chunk + * @param stride The stride of the chunk */ - public void unqueueChunk(int worldX, int worldY, int worldZ){ - String key = this.getKey(worldX,worldY,worldZ); + public void unqueueChunk(int worldX, int worldY, int worldZ, int stride){ + Long key = this.getKey(worldX,worldY,worldZ); lock.acquireUninterruptibly(); this.queuedChunkMap.remove(key); lock.release(); } + + /** + * Gets the cache + * @param stride The stride of the data + * @return The cache to use + */ + public Map getCache(int stride){ + switch(stride){ + case 0: { + return cacheMapFullRes; + } + case 1: { + return cacheMapHalfRes; + } + case 2: { + return cacheMapQuarterRes; + } + case 3: { + return cacheMapEighthRes; + } + case 4: { + return cacheMapSixteenthRes; + } + default: { + throw new Error("Invalid stride probided! " + stride); + } + } + } } diff --git a/src/main/java/electrosphere/server/terrain/manager/ServerTerrainManager.java b/src/main/java/electrosphere/server/terrain/manager/ServerTerrainManager.java index 2c9adec1..e022d83b 100644 --- a/src/main/java/electrosphere/server/terrain/manager/ServerTerrainManager.java +++ b/src/main/java/electrosphere/server/terrain/manager/ServerTerrainManager.java @@ -1,5 +1,6 @@ package electrosphere.server.terrain.manager; +import electrosphere.client.terrain.cache.ChunkData; import electrosphere.engine.Globals; import electrosphere.game.server.world.ServerWorldData; import electrosphere.server.terrain.diskmap.ChunkDiskMap; @@ -231,8 +232,8 @@ public class ServerTerrainManager { Globals.profiler.beginCpuSample("ServerTerrainManager.getChunk"); //THIS FIRES IF THERE IS A MAIN GAME WORLD RUNNING ServerTerrainChunk returnedChunk = null; - if(chunkCache.containsChunk(worldX,worldY,worldZ)){ - returnedChunk = chunkCache.get(worldX,worldY,worldZ); + if(chunkCache.containsChunk(worldX,worldY,worldZ,ChunkData.NO_STRIDE)){ + returnedChunk = chunkCache.get(worldX,worldY,worldZ, ChunkData.NO_STRIDE); } else { //pull from disk if it exists if(chunkDiskMap != null){ @@ -242,9 +243,9 @@ public class ServerTerrainManager { } //generate if it does not exist if(returnedChunk == null){ - returnedChunk = chunkGenerator.generateChunk(worldX, worldY, worldZ); + returnedChunk = chunkGenerator.generateChunk(worldX, worldY, worldZ, ChunkData.NO_STRIDE); } - this.chunkCache.add(worldX, worldY, worldZ, returnedChunk); + this.chunkCache.add(worldX, worldY, worldZ, ChunkData.NO_STRIDE, returnedChunk); } Globals.profiler.endCpuSample(); return returnedChunk; @@ -255,11 +256,12 @@ public class ServerTerrainManager { * @param worldX The world x position * @param worldY The world y position * @param worldZ The world z position + * @param stride The stride of the data * @param onLoad The logic to run once the chunk is available */ - public void getChunkAsync(int worldX, int worldY, int worldZ, Consumer onLoad){ + public void getChunkAsync(int worldX, int worldY, int worldZ, int stride, Consumer onLoad){ Globals.profiler.beginCpuSample("ServerTerrainManager.getChunkAsync"); - this.chunkExecutorService.submit(new ChunkGenerationThread(chunkDiskMap, chunkCache, chunkGenerator, worldX, worldY, worldZ, onLoad)); + this.chunkExecutorService.submit(new ChunkGenerationThread(chunkDiskMap, chunkCache, chunkGenerator, worldX, worldY, worldZ, stride, onLoad)); Globals.profiler.endCpuSample(); } @@ -287,8 +289,8 @@ public class ServerTerrainManager { if(model != null){ model.addModification(modification); } - if(chunkCache.containsChunk(worldPos.x,worldPos.y,worldPos.z)){ - ServerTerrainChunk chunk = chunkCache.get(worldPos.x,worldPos.y,worldPos.z); + if(chunkCache.containsChunk(worldPos.x,worldPos.y,worldPos.z,ChunkData.NO_STRIDE)){ + ServerTerrainChunk chunk = chunkCache.get(worldPos.x,worldPos.y,worldPos.z, ChunkData.NO_STRIDE); chunk.addModification(modification); } } diff --git a/src/main/java/electrosphere/server/terrain/models/TerrainModel.java b/src/main/java/electrosphere/server/terrain/models/TerrainModel.java index 7c97cb86..17f4757e 100644 --- a/src/main/java/electrosphere/server/terrain/models/TerrainModel.java +++ b/src/main/java/electrosphere/server/terrain/models/TerrainModel.java @@ -127,11 +127,13 @@ public class TerrainModel { TerrainModel rVal = new TerrainModel(); rVal.discreteArrayDimension = TestGenerationChunkGenerator.GENERATOR_REALM_SIZE; rVal.dynamicInterpolationRatio = 1; - rVal.biome = new short[2][2]; - rVal.biome[0][0] = TestGenerationChunkGenerator.DEFAULT_BIOME_INDEX; - rVal.biome[1][0] = TestGenerationChunkGenerator.DEFAULT_BIOME_INDEX; - rVal.biome[0][1] = TestGenerationChunkGenerator.DEFAULT_BIOME_INDEX; - rVal.biome[1][1] = TestGenerationChunkGenerator.DEFAULT_BIOME_INDEX; + int macroDataImageScale = TestGenerationChunkGenerator.GENERATOR_REALM_SIZE / DEFAULT_MACRO_DATA_SCALE + 1; + rVal.biome = new short[macroDataImageScale][macroDataImageScale]; + for(int x = 0; x < macroDataImageScale; x++){ + for(int z = 0; z < macroDataImageScale; z++){ + rVal.biome[x][z] = TestGenerationChunkGenerator.DEFAULT_BIOME_INDEX; + } + } return rVal; }