package electrosphere.client.terrain.cells; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.joml.Vector3d; import org.joml.Vector3i; import electrosphere.client.terrain.cache.ChunkData; import electrosphere.client.terrain.cells.DrawCell.DrawCellFace; import electrosphere.client.terrain.manager.ClientTerrainManager; import electrosphere.engine.Globals; import electrosphere.entity.EntityUtils; import electrosphere.net.parser.net.message.TerrainMessage; import electrosphere.renderer.shader.ShaderProgram; import electrosphere.server.terrain.manager.ServerTerrainChunk; /** * Manages the graphical entities for the terrain chunks * * * Notes for integrating with transvoxel algo: Different problems to tackle For all chunks within minimum radius, check if they can be updated and update accordingly <--- we do this currently For all chunks between minimum radius and first LOD radius, check if we can make a LOD chunk If we can, make a lod chunk or see if it can be updated This check will be: For every position, check if all four positions required for LOD chunk are within lod radius if yes, make lod chunk If we cannot, create a fullres chunk This check will be: For every position, check if all four positions required for LOD chunk are within lod radius if yes, make lod chunk if they are outside the far bound, create lod chunk if they are within the near bound, create a fullres chunk */ public class DrawCellManager { //the center of this cell manager's array in cell space int cellX; int cellY; int cellZ; //all currently displaying mini cells Set cells = new HashSet(); Map keyCellMap = new HashMap(); //status of all position keys Set hasNotRequested = new HashSet(); Set requested = new HashSet(); Set drawable = new HashSet(); Set undrawable = new HashSet(); Set updateable = new HashSet(); //LOD level of all position keys Map positionLODLevel = new HashMap(); //voxel atlas VoxelTextureAtlas atlas; //shader program for drawable cells ShaderProgram program; //the real-space radius for which we will construct draw cells inside of //ie, we check if the draw cell's entity would be inside this radius. If it would, create the draw cell, otherwise don't double drawFullModelRadius = 50; //the radius we'll draw LODed chunks for double drawLODRadius = drawFullModelRadius + ServerTerrainChunk.CHUNK_DIMENSION * (2*2 + 4*4 + 8*8 + 16*16); //the number of possible LOD levels //1,2,4,8,16 static final int NUMBER_OF_LOD_LEVELS = 5; //the table of lod leve -> radius at which we will look for chunks within this log double[] lodLevelRadiusTable = new double[5]; //the radius for which physics meshes are created when draw cells are created int physicsRadius = 3; //ready to start updating? boolean update = false; //controls whether we try to generate the drawable entities //we want this to be false when in server-only mode boolean generateDrawables = false; /** * DrawCellManager constructor * @param commonWorldData The common world data * @param clientTerrainManager The client terrain manager * @param discreteX The initial discrete position X coordinate * @param discreteY The initial discrete position Y coordinate */ public DrawCellManager(ClientTerrainManager clientTerrainManager, int discreteX, int discreteY, int discreteZ){ cellX = discreteX; cellY = discreteY; cellZ = discreteZ; program = Globals.terrainShaderProgram; //the first lod level is set by user lodLevelRadiusTable[0] = drawFullModelRadius; //generate LOD radius table for(int i = 1; i < NUMBER_OF_LOD_LEVELS; i++){ double sizeOfSingleModel = Math.pow(2,i) * ServerTerrainChunk.CHUNK_DIMENSION; //size of the radius for this lod level should be three times the size of a model + the previous radius //this guarantees we get at least one adapter chunk, one proper chunk, and also that the radius accounts for the previous lod level chunks lodLevelRadiusTable[i] = lodLevelRadiusTable[i-1] + sizeOfSingleModel * 3; } physicsRadius = Globals.userSettings.getGameplayPhysicsCellRadius(); invalidateAllCells(); update = true; } /** * Private constructor */ DrawCellManager(){ } /** * Sets the player's current position in cell-space * @param cellPos The cell's position */ public void setPlayerCell(Vector3i cellPos){ cellX = cellPos.x; cellY = cellPos.y; cellZ = cellPos.z; } /** * Update function that is called if a cell has not been requested */ void updateUnrequestedCell(){ if(hasNotRequested.size() > 0){ String targetKey = hasNotRequested.iterator().next(); hasNotRequested.remove(targetKey); Vector3i worldPos = getVectorFromKey(targetKey); // //Because of the way marching cubes works, we need to request the adjacent chunks so we know how to properly blend between one chunk and the next //The following loop-hell does this // 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(worldPos).add(i,j,k); String requestKey = getCellKey(posToCheck.x,posToCheck.y,posToCheck.z); 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) ){ if(!requested.contains(requestKey)){ //client should request chunk data from server for each chunk necessary to create the model Globals.clientConnection.queueOutgoingMessage(TerrainMessage.constructRequestChunkDataMessage( posToCheck.x, posToCheck.y, posToCheck.z )); } } } } } undrawable.add(targetKey); requested.add(targetKey); } } /** * Makes one of the undrawable cells drawable */ void makeCellDrawable(){ if(undrawable.size() > 0){ String targetKey = undrawable.iterator().next(); Vector3i worldPos = getVectorFromKey(targetKey); // //Checks if all chunk data necessary to generate a mesh is present boolean containsNecessaryChunks = true; 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(worldPos).add(i,j,k); if(worldPos.x >= 0 && posToCheck.x < Globals.clientWorldData.getWorldDiscreteSize() && posToCheck.y >= 0 && posToCheck.y < Globals.clientWorldData.getWorldDiscreteSize() && posToCheck.z >= 0 && posToCheck.z < Globals.clientWorldData.getWorldDiscreteSize() && !containsChunkDataAtWorldPoint(posToCheck.x,posToCheck.y,posToCheck.z) ){ containsNecessaryChunks = false; } } } } // //if contains data for all chunks necessary to generate visuals if(containsNecessaryChunks){ //update the status of the terrain key undrawable.remove(targetKey); drawable.add(targetKey); //build the cell DrawCell cell = DrawCell.generateTerrainCell( worldPos ); cells.add(cell); keyCellMap.put(targetKey,cell); DrawCellFace higherLODFace = null; keyCellMap.get(targetKey).generateDrawableEntity(atlas,0,higherLODFace); //evaluate for foliage Globals.clientFoliageManager.evaluateChunk(worldPos); } } } /** * Updates a cell that can be updated */ void updateCellModel(){ if(updateable.size() > 0){ String targetKey = updateable.iterator().next(); updateable.remove(targetKey); Vector3i worldPos = getVectorFromKey(targetKey); if( worldPos.x >= 0 && worldPos.x < Globals.clientWorldData.getWorldDiscreteSize() && worldPos.y >= 0 && worldPos.y < Globals.clientWorldData.getWorldDiscreteSize() && worldPos.z >= 0 && worldPos.z < Globals.clientWorldData.getWorldDiscreteSize() ){ keyCellMap.get(targetKey).destroy(); DrawCellFace higherLODFace = null; keyCellMap.get(targetKey).generateDrawableEntity(atlas,0,higherLODFace); } drawable.add(targetKey); } } /** * Checks if the manager contains a cell position that hasn't had its chunk data requested from the server yet * @return true if there is an unrequested cell, false otherwise */ public boolean containsUnrequestedCell(){ return hasNotRequested.size() > 0; } /** * Checks if the manager contains a cell who hasn't been made drawable yet * @return true if there is an undrawable cell, false otherwise */ public boolean containsUndrawableCell(){ return undrawable.size() > 0; } /** * Checks if the manager contains a cell who needs to be updated * @return true if there is an updateable cell, false otherwise */ public boolean containsUpdateableCell(){ return updateable.size() > 0; } /** * Transforms a real coordinate into a cell-space coordinate * @param input the real coordinate * @return the cell coordinate */ public int transformRealSpaceToCellSpace(double input){ return (int)(input / Globals.clientWorldData.getDynamicInterpolationRatio()); } /** * Clears the valid set and adds all keys to invalid set */ public void invalidateAllCells(){ drawable.clear(); hasNotRequested.clear(); clearOutOfBoundsCells(); queueNewCells(); } /** * Calculates whether the position of the player has changed and if so, invalidates and cleans up cells accordingly */ private void calculateDeltas(){ //check if any not requested cells no longer need to be requested clearOutOfBoundsCells(); //check if any cells should be added queueNewCells(); } /** * Clears all cells outside of draw radius */ private void clearOutOfBoundsCells(){ Set cellsToRemove = new HashSet(); for(DrawCell cell : cells){ Vector3d realPos = cell.getRealPos(); if(Globals.playerEntity != null && EntityUtils.getPosition(Globals.playerEntity).distance(realPos) > drawFullModelRadius){ cellsToRemove.add(cell); } } for(DrawCell cell : cellsToRemove){ cells.remove(cell); String key = getCellKey(cell.worldPos.x, cell.worldPos.y, cell.worldPos.z); hasNotRequested.remove(key); drawable.remove(key); undrawable.remove(key); updateable.remove(key); keyCellMap.remove(key); requested.remove(key); cell.destroy(); } } /** * Queues new cells that are in bounds but not currently accounted for */ private void queueNewCells(){ if(Globals.playerEntity != null && Globals.clientWorldData != null){ Vector3d playerPos = EntityUtils.getPosition(Globals.playerEntity); for(int x = -(int)drawFullModelRadius; x < drawFullModelRadius; x = x + ChunkData.CHUNK_SIZE){ for(int y = -(int)drawFullModelRadius; y < drawFullModelRadius; y = y + ChunkData.CHUNK_SIZE){ for(int z = -(int)drawFullModelRadius; z < drawFullModelRadius; z = z + ChunkData.CHUNK_SIZE){ Vector3d newPos = new Vector3d(playerPos.x + x, playerPos.y + y, playerPos.z + z); Vector3i worldPos = new Vector3i( Globals.clientWorldData.convertRealToChunkSpace(newPos.x), Globals.clientWorldData.convertRealToChunkSpace(newPos.y), Globals.clientWorldData.convertRealToChunkSpace(newPos.z) ); Vector3d chunkRealSpace = new Vector3d( Globals.clientWorldData.convertChunkToRealSpace(worldPos.x), Globals.clientWorldData.convertChunkToRealSpace(worldPos.y), Globals.clientWorldData.convertChunkToRealSpace(worldPos.z) ); if( playerPos.distance(chunkRealSpace) < drawFullModelRadius && worldPos.x >= 0 && worldPos.x < Globals.clientWorldData.getWorldDiscreteSize() && worldPos.y >= 0 && worldPos.y < Globals.clientWorldData.getWorldDiscreteSize() && worldPos.z >= 0 && worldPos.z < Globals.clientWorldData.getWorldDiscreteSize() ){ String key = getCellKey( Globals.clientWorldData.convertRealToChunkSpace(chunkRealSpace.x), Globals.clientWorldData.convertRealToChunkSpace(chunkRealSpace.y), Globals.clientWorldData.convertRealToChunkSpace(chunkRealSpace.z) ); if(!keyCellMap.containsKey(key) && !hasNotRequested.contains(key) && !undrawable.contains(key) && !drawable.contains(key) && !requested.contains(key)){ hasNotRequested.add(key); } } } } } } } /** * Updates cells that need updating in this manager */ public void update(){ calculateDeltas(); if(containsUnrequestedCell() && !containsUndrawableCell()){ updateUnrequestedCell(); } else if(containsUndrawableCell()){ makeCellDrawable(); } else if(containsUpdateableCell()){ updateCellModel(); } } /** * Controls whether the client generates drawable chunks or just physics chunks (ie if running a headless client) * @param generate true to generate graphics, false otherwise */ public void setGenerateDrawables(boolean generate){ this.generateDrawables = generate; } /** * 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; } /** * Gets the chunk data at a given point * @param worldX The world position x component of the cell * @param worldY The world position y component of the cell * @param worldZ The world position z component of the cell * @return The chunk data at the specified points */ ChunkData getChunkDataAtPoint(int worldX, int worldY, int worldZ){ return Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldX,worldY,worldZ); } /** * Gets a unique key for the cell * @param worldX The world position x component of the cell * @param worldY The world position y component of the cell * @param worldZ The world position z component of the cell * @return The key */ private String getCellKey(int worldX, int worldY, int worldZ){ return worldX + "_" + worldY + "_" + worldZ; } /** * Parses a vector3i from the cell key * @param key The cell key * @return The vector3i containing the components of the cell key */ private Vector3i getVectorFromKey(String key){ String[] keyComponents = key.split("_"); return new Vector3i(Integer.parseInt(keyComponents[0]),Integer.parseInt(keyComponents[1]),Integer.parseInt(keyComponents[2])); } /** * Marks a data cell as updateable (can be regenerated with a new model because the underlying data has changed) * @param chunkX The chunk x coordinate * @param chunkY The chunk y coordinate * @param chunkZ The chunk z coordinate */ public void markUpdateable(int chunkX, int chunkY, int chunkZ){ updateable.add(getCellKey(chunkX, chunkY, chunkZ)); } /** * Gets the radius within which full-detail models are drawn * @return the radius */ public double getDrawFullModelRadius(){ return drawFullModelRadius; } /** * Initializes the voxel texture atlas */ public void attachTextureAtlas(VoxelTextureAtlas voxelTextureAtlas){ atlas = voxelTextureAtlas; } }