package electrosphere.client.foliagemanager; import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; 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 org.lwjgl.BufferUtils; 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.actor.instance.TextureInstancedActor; import electrosphere.renderer.buffer.ShaderAttribute; import electrosphere.renderer.buffer.HomogenousUniformBuffer.HomogenousBufferTypes; import electrosphere.renderer.texture.Texture; /** * Manages ambient foliage (grass, small plants, etc) that should be shown, typically instanced */ public class ClientFoliageManager { //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 = 15f; //The maximum number of foliage cells static final int CELL_COUNT_MAX = 1000; //the interval to space along static final int TARGET_FOLIAGE_SPACING = 50; //The target number of foliage to place per cell static final int TARGET_FOLIAGE_PER_CELL = TARGET_FOLIAGE_SPACING * TARGET_FOLIAGE_SPACING; //size of a single item of foliage in the texture buffer /* * A lot of these are x 4 to account for size of float * 3 x 4 for position * 2 x 4 for euler rotation * * * eventually: * grass type * color * wind characteristics? */ static final int SINGLE_FOLIAGE_DATA_SIZE_BYTES = 3 * 4 + 2 * 4; //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()); Globals.assetManager.addShaderToQueue(vertexPath, fragmentPath); } } ready = true; } /** * Updates all grass entities */ public void update(){ Globals.profiler.beginCpuSample("ClientFoliageManager.update"); if(ready){ Vector3d playerPosition = null; if(Globals.playerEntity != null){ playerPosition = EntityUtils.getPosition(Globals.playerEntity); } Globals.profiler.beginCpuSample("ClientFoliageManager.update (talley invalid cells)"); //flip through all valid cells and see if you can invalidate any List invalidationList = new LinkedList(); for(FoliageCell cell : activeCells){ if( playerPosition != null && playerPosition.distance(cell.realPosition) > CELL_DISTANCE_MAX ){ invalidationList.add(cell); } } Globals.profiler.endCpuSample(); Globals.profiler.beginCpuSample("ClientFoliageManager.update (actually invalidate cells)"); for(FoliageCell cell : invalidationList){ invalidateCell(cell); } Globals.profiler.endCpuSample(); invalidationList.clear(); //for each invalid cell, see if can be revalidated Globals.profiler.beginCpuSample("ClientFoliageManager.update (revalidate cells)"); 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])); Vector3d realPos = Globals.clientWorldData.convertWorldToRealSpace(worldPos).add(voxelPos.x,voxelPos.y,voxelPos.z); ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos); //evaluate if( data != null && data.getWeight(voxelPos) > 0 && data.getWeight(new Vector3i(voxelPos.x,voxelPos.y + 1,voxelPos.z)) < 0 && typeSupportsFoliage(data.getType(voxelPos)) && playerPosition.distance(realPos) < CELL_DISTANCE_MAX ){ //create foliage cell createFoliageCell(worldPos,voxelPos,1); locationEvaluationCooldownMap.remove(key); } else { locationEvaluationCooldownMap.put(key, EVALUATION_COOLDOWN); } } else { locationEvaluationCooldownMap.put(key, cooldownTime); } } Globals.profiler.endCpuSample(); //invalidate foliage cells that have had their voxel changed invalidateModifiedPositions(); } Globals.profiler.endCpuSample(); } /** * 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(); 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()).normalize(); } /** * 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 makeEntityTextureInstancedFoliage(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)) && activeCells.size() < CELL_COUNT_MAX ){ //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)) && activeCells.size() < CELL_COUNT_MAX ){ //create foliage cell createFoliageCell(worldPos,currentPos,1); } } } } } } //the length of the ray to ground test with static final float RAY_LENGTH = 2.0f; //the height above the chunk to start from when sampling downwards static final float SAMPLE_START_HEIGHT = 1.0f; /** * 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){ Vector3d realPos = new Vector3d( worldPos.x * ChunkData.CHUNK_SIZE + voxelPos.x, worldPos.y * ChunkData.CHUNK_SIZE + voxelPos.y, worldPos.z * ChunkData.CHUNK_SIZE + voxelPos.z ); //get type String foliageTypeName = foliageTypesSupported.get(placementRandomizer.nextInt() % foliageTypesSupported.size()); FoliageType foliageType = Globals.gameConfigCurrent.getFoliageMap().getFoliage(foliageTypeName); //create cell and buffer FoliageCell cell = new FoliageCell(worldPos, voxelPos, realPos); ByteBuffer buffer = BufferUtils.createByteBuffer(TARGET_FOLIAGE_SPACING * TARGET_FOLIAGE_SPACING * SINGLE_FOLIAGE_DATA_SIZE_BYTES); FloatBuffer floatBufferView = buffer.asFloatBuffer(); //construct simple grid to place foliage on Vector3d sample_00 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add(-0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH); Vector3d sample_01 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add(-0.5,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH); Vector3d sample_02 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add(-0.5,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH); Vector3d sample_10 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add( 0,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH); Vector3d sample_11 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add( 0,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH); Vector3d sample_12 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add( 0,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH); Vector3d sample_20 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add( 0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH); Vector3d sample_21 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add( 0.5,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH); Vector3d sample_22 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add( 0.5,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH); //get the heights of each sample float height_11 = (float)(sample_11 != null ? sample_11.y : 0); float height_00 = (float)(sample_00 != null ? sample_00.y : height_11); float height_01 = (float)(sample_01 != null ? sample_01.y : height_11); float height_02 = (float)(sample_02 != null ? sample_02.y : height_11); float height_10 = (float)(sample_10 != null ? sample_10.y : height_11); float height_12 = (float)(sample_12 != null ? sample_12.y : height_11); float height_20 = (float)(sample_20 != null ? sample_20.y : height_11); float height_21 = (float)(sample_21 != null ? sample_21.y : height_11); float height_22 = (float)(sample_22 != null ? sample_22.y : height_11); //each height is in real world coordinates that are absolute //when rendering, there's already a y offset for the center of the field of grass (based on the model matrix) //so when offseting the position of the blade of grass RELATIVE to the overall instance being drawn, need to subtract the real world coordinates of the overall instance //in other words realPos SPECIFICALLY for the y dimension, for x and z you don't need to worry about it //if we don't find data for the center sample, can't place grass so don't create entity if(sample_11 != null){ //generate positions to place int drawCount = 0; for(int x = 0; x < TARGET_FOLIAGE_SPACING; x++){ for(int z = 0; z < TARGET_FOLIAGE_SPACING; z++){ //get position to place double rand1 = placementRandomizer.nextDouble(); double rand2 = placementRandomizer.nextDouble(); double relativePositionOnGridX = x / (1.0 * TARGET_FOLIAGE_SPACING) + rand1 / TARGET_FOLIAGE_SPACING; double relativePositionOnGridZ = z / (1.0 * TARGET_FOLIAGE_SPACING) + rand2 / TARGET_FOLIAGE_SPACING; double offsetX = relativePositionOnGridX - 0.5; double offsetZ = relativePositionOnGridZ - 0.5; //determine quadrant we're placing in double offsetY = 0; boolean addBlade = false; if(relativePositionOnGridX >=0.5){ if(relativePositionOnGridZ >= 0.5){ relativePositionOnGridX = relativePositionOnGridX - 0.5; relativePositionOnGridZ = relativePositionOnGridZ - 0.5; relativePositionOnGridX /= 0.5; relativePositionOnGridZ /= 0.5; // System.out.println(relativePositionOnGridX + " " + relativePositionOnGridZ); //if we have heights for all four surrounding spots, interpolate for y value if(sample_11 != null && sample_12 != null && sample_21 != null && sample_22 != null){ offsetY = height_11 * (1-relativePositionOnGridX) * (1-relativePositionOnGridZ) + height_12 * (1-relativePositionOnGridX) * ( relativePositionOnGridZ) + height_21 * ( relativePositionOnGridX) * (1-relativePositionOnGridZ) + height_22 * ( relativePositionOnGridX) * ( relativePositionOnGridZ); addBlade = true; } } else { relativePositionOnGridX = relativePositionOnGridX - 0.5; relativePositionOnGridX /= 0.5; relativePositionOnGridZ /= 0.5; //if we have heights for all four surrounding spots, interpolate for y value if(sample_10 != null && sample_11 != null && sample_20 != null && sample_21 != null){ offsetY = height_10 * (1-relativePositionOnGridX) * (1-relativePositionOnGridZ) + height_11 * (1-relativePositionOnGridX) * ( relativePositionOnGridZ) + height_20 * ( relativePositionOnGridX) * (1-relativePositionOnGridZ) + height_21 * ( relativePositionOnGridX) * ( relativePositionOnGridZ); addBlade = true; } } } else { if(relativePositionOnGridZ >= 0.5){ relativePositionOnGridZ = relativePositionOnGridZ - 0.5; relativePositionOnGridX /= 0.5; relativePositionOnGridZ /= 0.5; //if we have heights for all four surrounding spots, interpolate for y value if(sample_01 != null && sample_02 != null && sample_11 != null && sample_12 != null){ offsetY = height_01 * (1-relativePositionOnGridX) * (1-relativePositionOnGridZ) + height_02 * (1-relativePositionOnGridX) * ( relativePositionOnGridZ) + height_11 * ( relativePositionOnGridX) * (1-relativePositionOnGridZ) + height_12 * ( relativePositionOnGridX) * ( relativePositionOnGridZ); addBlade = true; } } else { relativePositionOnGridX /= 0.5; relativePositionOnGridZ /= 0.5; //if we have heights for all four surrounding spots, interpolate for y value if(sample_00 != null && sample_01 != null && sample_10 != null && sample_11 != null){ offsetY = height_00 * (1-relativePositionOnGridX) * (1-relativePositionOnGridZ) + height_01 * (1-relativePositionOnGridX) * ( relativePositionOnGridZ) + height_10 * ( relativePositionOnGridX) * (1-relativePositionOnGridZ) + height_11 * ( relativePositionOnGridX) * ( relativePositionOnGridZ); addBlade = true; } } } if(addBlade){ //convert y to relative to chunk offsetY = offsetY - realPos.y; double rotVar = placementRandomizer.nextDouble() * Math.PI * 2; double rotVar2 = placementRandomizer.nextDouble(); floatBufferView.put((float)offsetX); floatBufferView.put((float)offsetY); floatBufferView.put((float)offsetZ); floatBufferView.put((float)rotVar); floatBufferView.put((float)rotVar2); drawCount++; } } } buffer.position(0); buffer.limit(TARGET_FOLIAGE_SPACING * TARGET_FOLIAGE_SPACING * SINGLE_FOLIAGE_DATA_SIZE_BYTES); //construct data texture Texture dataTexture = new Texture(buffer,SINGLE_FOLIAGE_DATA_SIZE_BYTES / 4,TARGET_FOLIAGE_SPACING * TARGET_FOLIAGE_SPACING); //create entity Entity grassEntity = EntityCreationUtils.createClientSpatialEntity(); TextureInstancedActor.attachTextureInstancedActor(grassEntity, foliageType.getModelPath(), vertexPath, fragmentPath, dataTexture, drawCount); EntityUtils.getPosition(grassEntity).set(realPos); EntityUtils.getRotation(grassEntity).set(0,0,0,1); EntityUtils.getScale(grassEntity).set(1,1,1); //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 a cell by direct reference * @param cell The cell to invalidate */ private void invalidateCell(FoliageCell cell){ String key = getFoliageCellKey(cell.worldPosition, cell.voxelPosition); if(!locationEvaluationCooldownMap.containsKey(key)){ locationEvaluationCooldownMap.put(key,EVALUATION_COOLDOWN); } //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(); } } } /** * Draws all foliage in the foliage manager */ public void draw(){ for(FoliageCell cell : activeCells){ cell.draw(modelMatrixAttribute); } } }