package electrosphere.client.foliagemanager; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.joml.Quaterniond; import org.joml.Vector3d; import org.joml.Vector3f; import org.joml.Vector3i; import electrosphere.client.terrain.cache.ChunkData; import electrosphere.engine.Globals; import electrosphere.entity.Entity; import electrosphere.entity.EntityCreationUtils; import electrosphere.entity.EntityDataStrings; import electrosphere.entity.EntityTags; import electrosphere.entity.EntityUtils; import electrosphere.entity.state.foliage.AmbientFoliage; import electrosphere.game.data.foliage.type.FoliageType; import electrosphere.renderer.buffer.ShaderAttribute; import electrosphere.renderer.buffer.HomogenousUniformBuffer.HomogenousBufferTypes; /** * Manages ambient foliage (grass, small plants, etc) that should be shown, typically instanced */ public class ClientFoliageManager { //threshold for a grass blade to relocate to a new position static final float GRASS_RELOCATION_THRESHOLD = 5f; //amount of grass entities to manage static final int grassCapacity = 10000; //Random for finding new positions for foliage Random placementRandomizer = new Random(); //Used to prevent concurrent usage of grassEntities set boolean ready = false; //The list of voxel type ids that should have grass generated on top of them static final List grassGeneratingVoxelIds = new ArrayList(); //FoliageCells that are active and have foliage that is being drawn Set activeCells = new HashSet(); //map of position-based key to foliage cell at the position Map locationCellMap = new HashMap(); //The maximum distance a cell can be away from the player before being destroyed static final float CELL_DISTANCE_MAX = 25f; //The maximum number of foliage cells static final int CELL_COUNT_MAX = 100; //The target number of foliage to place per cell static final int TARGET_FOLIAGE_PER_CELL = 200; //Stores a list of all locations that are currently invalid which map to //the amount of frames that must pass before they are considered valid to evaluate Map locationEvaluationCooldownMap = new ConcurrentHashMap(); //The number of frames that must pass before a cell can be reevaluated for foliage placement static final int EVALUATION_COOLDOWN = 100; //The map of all attributes for instanced foliage static final Map attributes = new HashMap(); //model matrix shader attribute static ShaderAttribute modelMatrixAttribute; //set attributes static { int[] attributeIndices = new int[]{ 5,6,7,8 }; modelMatrixAttribute = new ShaderAttribute(attributeIndices); attributes.put(modelMatrixAttribute,HomogenousBufferTypes.MAT4F); //set grass generating voxel ids grassGeneratingVoxelIds.add(2); } //shader paths static final String vertexPath = "shaders/foliage/foliage.vs"; static final String fragmentPath = "shaders/foliage/foliage.fs"; /** * Starts up the foliage manager */ public void start(){ //queue ambient foliage models for(FoliageType foliageType : Globals.gameConfigCurrent.getFoliageMap().getFoliageList()){ if(foliageType.getTokens().contains(FoliageType.TOKEN_AMBIENT)){ Globals.assetManager.addModelPathToQueue(foliageType.getModelPath()); } } ready = true; } /** * Updates all grass entities */ public void update(){ if(ready){ //TODO: frustum cull at cell level before individual model level //to be clear, these blades are frustum culled 1-by-1 currently at the InstancedActor level //if we frustum cull at cell level with priority updates around then, we can pack foliage into buffer in chunks //and maintain the size of chunks and location on cpu //then use opengl calls to buffer only occasionally //if we're doing that we can also increase the amount and type of data (would be really nice to include position-in-chunk of foliage //for instance for fancy shaders based on local position (think rainbow grass constantly pulsing)) //this will inherit all the difficulty of defragmenting the buffer, turning individual chunks on and off on gpu side, etc //a way to save on sending position buffer to gpu would be to pack voxel pos into a smaller size ie use bitwise operators to store //local coord in 1 * 4 bytes instead of 3 * 4 for a full vector for(FoliageCell cell : activeCells){ cell.draw(modelMatrixAttribute); } //for each invalid cell, see if can be revalidated for(String key : locationEvaluationCooldownMap.keySet()){ int cooldownTime = locationEvaluationCooldownMap.get(key); cooldownTime--; if(cooldownTime <= 0){ String split[] = key.split("_"); Vector3i worldPos = new Vector3i(Integer.parseInt(split[0]),Integer.parseInt(split[1]),Integer.parseInt(split[2])); Vector3i voxelPos = new Vector3i(Integer.parseInt(split[3]),Integer.parseInt(split[4]),Integer.parseInt(split[5])); ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos); //evaluate if( data.getWeight(voxelPos) > 0 && data.getWeight(new Vector3i(voxelPos.x,voxelPos.y + 1,voxelPos.z)) < 0 && typeSupportsFoliage(data.getType(voxelPos)) ){ //create foliage cell createFoliageCell(worldPos,voxelPos,0); } locationEvaluationCooldownMap.remove(key); } else { locationEvaluationCooldownMap.put(key, cooldownTime); } } //invalidate foliage cells that have had their voxel changed invalidateModifiedPositions(); } } /** * Gets a good position to put a new blade of grass * @param centerPosition The player's position * @return The new position for the blade of grass */ protected Vector3d getNewPosition(Vector3d centerPosition){ double angle = placementRandomizer.nextDouble() * Math.PI * 2; double radius = placementRandomizer.nextDouble() * GRASS_RELOCATION_THRESHOLD; return new Vector3d( centerPosition.x + Math.cos(angle) * radius, centerPosition.y, centerPosition.z + Math.sin(angle) * radius ); } /** * Gets a new rotation for a blade of grass * @return The rotation */ protected Quaterniond getNewRotation(){ return new Quaterniond().rotationX(-Math.PI / 2.0f).rotateLocalY(Math.PI * placementRandomizer.nextFloat()); } /** * Gets a key for a foliage cell in the localCellMap * @param worldPosition The world position of the cell * @param voxelPosition The voxel position of the cell * @return The key for the cell */ private String getFoliageCellKey(Vector3i worldPosition, Vector3i voxelPosition){ return worldPosition.x + "_" + worldPosition.y + "_" + worldPosition.z + "_" + voxelPosition.x + "_" + voxelPosition.y + "_" + voxelPosition.z; } /** * Makes an already created entity a drawable, instanced entity (client only) by backing it with an InstancedActor * @param entity The entity * @param modelPath The model path for the model to back the instanced actor * @param capacity The capacity of the instanced actor to draw */ public static void makeEntityInstancedFoliage(Entity entity, String modelPath, int capacity){ entity.putData(EntityDataStrings.INSTANCED_ACTOR, Globals.clientInstanceManager.createInstancedActor(modelPath, vertexPath, fragmentPath, attributes, capacity)); entity.putData(EntityDataStrings.DATA_STRING_POSITION, new Vector3d(0,0,0)); entity.putData(EntityDataStrings.DATA_STRING_ROTATION, new Quaterniond().identity()); entity.putData(EntityDataStrings.DATA_STRING_SCALE, new Vector3f(1,1,1)); entity.putData(EntityDataStrings.DRAW_SOLID_PASS, true); Globals.clientScene.registerEntity(entity); Globals.clientScene.registerEntityToTag(entity, EntityTags.DRAW_INSTANCED_MANAGED); } /** * Evaluates a chunk to see where foliage cells should be created or updated * @param worldPos The world position of the chunk */ public void evaluateChunk(Vector3i worldPos){ ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos); for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){ //can't go to very top 'cause otherwise there would be no room to put grass for(int y = 0; y < ChunkData.CHUNK_SIZE - 1; y++){ for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){ Vector3i currentPos = new Vector3i(x,y,z); String key = getFoliageCellKey(worldPos, currentPos); if(locationCellMap.get(key) != null){ //destroy if there's no longer ground or //if the cell above is now occupied or //if the lower cell is no longer supporting foliage if( data.getWeight(currentPos) <= 0 || data.getWeight(new Vector3i(x,y + 1,z)) > 0 || !typeSupportsFoliage(data.getType(currentPos)) ){ //destroy FoliageCell toDestroy = locationCellMap.get(key); toDestroy.destroy(); activeCells.remove(toDestroy); locationCellMap.remove(key); } else { //TODO: evaluate if foliage is placed well } } else { //create if current is ground and above is air if( !locationEvaluationCooldownMap.containsKey(key) && data.getWeight(currentPos) > 0 && data.getWeight(new Vector3i(x,y + 1,z)) < 0 && typeSupportsFoliage(data.getType(currentPos)) ){ //create foliage cell createFoliageCell(worldPos,currentPos,1); } } } } } //evaluate top cells if chunk above this one exists ChunkData aboveData = Globals.clientTerrainManager.getChunkDataAtWorldPoint(new Vector3i(worldPos).add(0,1,0)); if(aboveData != null){ for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){ for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){ Vector3i currentPos = new Vector3i(x,ChunkData.CHUNK_SIZE-1,z); String key = getFoliageCellKey(worldPos, currentPos); if(locationCellMap.get(key) != null){ //destroy if there's no longer ground or //if the cell above is now occupied or //if the lower cell is no longer supporting foliage if( data.getWeight(currentPos) <= 0 || aboveData.getWeight(new Vector3i(x,0,z)) > 0 || !typeSupportsFoliage(data.getType(currentPos)) ){ //destroy FoliageCell toDestroy = locationCellMap.get(key); toDestroy.destroy(); activeCells.remove(toDestroy); locationCellMap.remove(key); } else { //TODO: evaluate if foliage is placed well } } else { //create if current is ground and above is air if( data.getWeight(currentPos) > 0 && aboveData.getWeight(new Vector3i(x,0,z)) < 0 && typeSupportsFoliage(data.getType(currentPos)) ){ //create foliage cell createFoliageCell(worldPos,currentPos,1); } } } } } } /** * Creates a foliage cell at a given position * @param worldPos The world position * @param voxelPos The voxel position */ private void createFoliageCell(Vector3i worldPos, Vector3i voxelPos, float initialGrowthLevel){ //get foliage types supported ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos); List foliageTypesSupported = Globals.gameConfigCurrent.getVoxelData().getTypeFromId(data.getType(voxelPos)).getAmbientFoliage(); if(foliageTypesSupported != null){ FoliageCell cell = new FoliageCell(worldPos, voxelPos); //create center foliage for(int i = 0; i < TARGET_FOLIAGE_PER_CELL; i++){ //get type String foliageTypeName = foliageTypesSupported.get(placementRandomizer.nextInt() % foliageTypesSupported.size()); FoliageType foliageType = Globals.gameConfigCurrent.getFoliageMap().getFoliage(foliageTypeName); //get position to place double offsetX = placementRandomizer.nextDouble() - 0.5; double offsetY = 2; double offsetZ = placementRandomizer.nextDouble() - 0.5; // double offsetY = placeFoliage(dataToConsider, offsetX, offsetZ, 0.2, 0.5, 0.15, 0.2+0.6); Vector3d testPosition = new Vector3d( worldPos.x * ChunkData.CHUNK_SIZE + voxelPos.x + offsetX, worldPos.y * ChunkData.CHUNK_SIZE + voxelPos.y + offsetY, worldPos.z * ChunkData.CHUNK_SIZE + voxelPos.z + offsetZ ); Vector3d placementPos = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(testPosition, new Vector3d(0,-1,0), 2.5); if(placementPos != null){ //create entity Entity grassEntity = EntityCreationUtils.createClientSpatialEntity(); makeEntityInstancedFoliage(grassEntity, foliageType.getModelPath(), grassCapacity); EntityUtils.getPosition(grassEntity).set(placementPos); EntityUtils.getRotation(grassEntity).set(getNewRotation()); EntityUtils.getScale(grassEntity).set(new Vector3d(2.0, 2.0, 2.0)); //add ambient foliage behavior tree AmbientFoliage.attachAmbientFoliageTree(grassEntity, initialGrowthLevel, foliageType.getGrowthModel().getGrowthRate()); cell.addEntity(grassEntity); } } activeCells.add(cell); locationCellMap.put(getFoliageCellKey(worldPos, voxelPos),cell); } } /** * Gets whether the voxel type supports foliage or not * @param type * @return */ private boolean typeSupportsFoliage(int type){ if(Globals.gameConfigCurrent.getVoxelData().getTypeFromId(type) != null){ return Globals.gameConfigCurrent.getVoxelData().getTypeFromId(type).getAmbientFoliage() != null; } return false; } /** * Invalidates a foliage cell at a position, destroying all foliage in the cell and unregistering it. * Furthermore, it adds it to a cooldown queue to wait until it can recreate foliage * @param worldPosition The world position of the cell * @param voxelPosition The voxel position of the cell */ private void invalidateCell(Vector3i worldPosition, Vector3i voxelPosition){ String key = getFoliageCellKey(worldPosition, voxelPosition); if(!locationEvaluationCooldownMap.containsKey(key)){ locationEvaluationCooldownMap.put(key,EVALUATION_COOLDOWN); FoliageCell cell = locationCellMap.get(key); if(cell != null){ //destroy FoliageCell toDestroy = locationCellMap.get(key); toDestroy.destroy(); activeCells.remove(toDestroy); locationCellMap.remove(key); } } } /** * Invalidates the foliage cells for all modified chunks */ private void invalidateModifiedPositions(){ for(ChunkData chunk : Globals.clientTerrainManager.getAllChunks()){ if(chunk.getModifiedPositions().size() > 0){ for(Vector3i position : chunk.getModifiedPositions()){ Globals.clientFoliageManager.invalidateCell(Globals.clientTerrainManager.getPositionOfChunk(chunk), position); } chunk.resetModifiedPositions(); } } } }