497 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
			
		
		
	
	
			497 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			Java
		
	
	
	
	
	
| 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<DrawCell> cells = new HashSet<DrawCell>();
 | |
|     Map<String,DrawCell> keyCellMap = new HashMap<String,DrawCell>();
 | |
|     
 | |
|     //status of all position keys
 | |
|     Set<String> hasNotRequested = new HashSet<String>();
 | |
|     Set<String> requested = new HashSet<String>();
 | |
|     Set<String> drawable = new HashSet<String>();
 | |
|     Set<String> undrawable = new HashSet<String>();
 | |
|     Set<String> updateable = new HashSet<String>();
 | |
| 
 | |
|     //LOD level of all position keys
 | |
|     Map<String,Integer> positionLODLevel = new HashMap<String,Integer>();
 | |
| 
 | |
|     //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<DrawCell> cellsToRemove = new HashSet<DrawCell>();
 | |
|         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;
 | |
|     }
 | |
| 
 | |
| 
 | |
|     
 | |
|     
 | |
| }
 |