diff --git a/buildNumber.properties b/buildNumber.properties index 3e3325f3..226a654f 100644 --- a/buildNumber.properties +++ b/buildNumber.properties @@ -1,3 +1,3 @@ #maven.buildNumber.plugin properties file -#Tue Sep 10 20:21:08 EDT 2024 -buildNumber=328 +#Wed Sep 11 17:27:47 EDT 2024 +buildNumber=329 diff --git a/src/main/java/electrosphere/client/foliagemanager/ClientFoliageManager.java b/src/main/java/electrosphere/client/foliagemanager/ClientFoliageManager.java index 417ccb2c..46e94125 100644 --- a/src/main/java/electrosphere/client/foliagemanager/ClientFoliageManager.java +++ b/src/main/java/electrosphere/client/foliagemanager/ClientFoliageManager.java @@ -1,124 +1,42 @@ 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; -import electrosphere.util.ds.Octree; -import electrosphere.util.ds.Octree.OctreeNode; /** * 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? + /** + * The target density for placing foliage */ - 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; - float targetDensity = 1.0f; + /** + * Number of chunks to check + */ + int chunkRadius = 1; + + /** - * The octree holding all the chunks to evaluate + * The list of all chunks with foliage, currently */ - Octree chunkTree; - - - //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/entities/foliage/foliage.vs"; - static final String fragmentPath = "Shaders/entities/foliage/foliage.fs"; - - - - - - - + List chunks = null; + /** + * Cache used to transfer still-valid chunks to the next frame + */ + List chunkUpdateCache = null; @@ -127,14 +45,9 @@ public class ClientFoliageManager { * 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); - } - } - this.chunkTree = new Octree(new Vector3d(0), new Vector3d(1000000000l)); + FoliageCell.init(); + chunks = new LinkedList(); + chunkUpdateCache = new LinkedList(); ready = true; } @@ -144,101 +57,56 @@ public class ClientFoliageManager { public void update(){ Globals.profiler.beginCpuSample("ClientFoliageManager.update"); if(ready && this.dependenciesAreReady()){ - Vector3d playerPosition = null; - if(Globals.playerEntity != null){ - playerPosition = EntityUtils.getPosition(Globals.playerEntity); - } - Vector3i worldPos = Globals.clientWorldData.convertRealToWorldSpace(playerPosition); - if(!chunkTree.containsLeaf(new Vector3d(worldPos))){ - this.createFoliageChunk(worldPos); - } - - // - //evaluate what cells should be updated - Globals.profiler.beginCpuSample("ClientFoliageManager.update (evaluate foliage chunks)"); - this.evaluateChunks(playerPosition, this.chunkTree.getRoot()); - Globals.profiler.endCpuSample(); - - - // - //flip through all valid cells and see if you can invalidate any - Globals.profiler.beginCpuSample("ClientFoliageManager.update (talley invalid cells)"); - List invalidationList = new LinkedList(); - for(FoliageCell cell : activeCells){ - if( - playerPosition != null && - playerPosition.distance(cell.realPosition) > CELL_DISTANCE_MAX - ){ - invalidationList.add(cell); - } - } - Globals.profiler.endCpuSample(); - - // - //actually invalidate cells - 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("_"); - 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,targetDensity); - locationEvaluationCooldownMap.remove(key); - } else { - locationEvaluationCooldownMap.put(key, EVALUATION_COOLDOWN); + this.flipUpdateCache(); + for(int x = -chunkRadius; x < chunkRadius+1; x++){ + for(int y = -chunkRadius; y < chunkRadius+1; y++){ + for(int z = -chunkRadius; z < chunkRadius+1; z++){ + Vector3i worldPos = Globals.clientWorldData.convertRealToWorldSpace(EntityUtils.getPosition(Globals.playerEntity)); + updatePosition(worldPos); } - } else { - locationEvaluationCooldownMap.put(key, cooldownTime); } } - Globals.profiler.endCpuSample(); - //invalidate foliage cells that have had their voxel changed - invalidateModifiedPositions(); + for(FoliageChunk chunk : this.chunkUpdateCache){ + chunk.destroy(); + } + this.chunkUpdateCache.clear(); } 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 + * Evaluates a position in the world + * @param worldPos The world position */ - 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 - ); + private void updatePosition(Vector3i worldPos){ + FoliageChunk foundChunk = null; + for(FoliageChunk chunk : chunkUpdateCache){ + if(chunk.getWorldPos().equals(worldPos)){ + this.chunks.add(chunk); + foundChunk = chunk; + break; + } + } + if(foundChunk == null){ + foundChunk = new FoliageChunk(worldPos); + this.chunks.add(foundChunk); + foundChunk.initCells(); + } else { + chunkUpdateCache.remove(foundChunk); + } + foundChunk.updateCells(); } /** - * Gets a new rotation for a blade of grass - * @return The rotation + * Flips references for the chunk list and chunk update cache */ - protected Quaterniond getNewRotation(){ - return new Quaterniond().rotationX(-Math.PI / 2.0f).rotateLocalY(Math.PI * placementRandomizer.nextFloat()).normalize(); + private void flipUpdateCache(){ + List list1 = this.chunks; + this.chunks = this.chunkUpdateCache; + this.chunkUpdateCache = list1; + if(this.chunks.size() > 0){ + throw new IllegalStateException("Update cache should have be empty and it isn't!"); + } } /** @@ -247,306 +115,10 @@ public class ClientFoliageManager { * @param voxelPosition The voxel position of the cell * @return The key for the cell */ - private String getFoliageCellKey(Vector3i worldPosition, Vector3i voxelPosition){ + protected static 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,targetDensity); - } - } - } - } - } - //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,targetDensity); - } - } - } - } - } - } - - //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, float density){ - //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, density); - int FINAL_SPACING = (int)(TARGET_FOLIAGE_SPACING * density); - ByteBuffer buffer = BufferUtils.createByteBuffer(FINAL_SPACING * FINAL_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 < FINAL_SPACING; x++){ - for(int z = 0; z < FINAL_SPACING; z++){ - //get position to place - double rand1 = placementRandomizer.nextDouble(); - double rand2 = placementRandomizer.nextDouble(); - double relativePositionOnGridX = x / (1.0 * FINAL_SPACING) + rand1 / FINAL_SPACING; - double relativePositionOnGridZ = z / (1.0 * FINAL_SPACING) + rand2 / FINAL_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(FINAL_SPACING * FINAL_SPACING * SINGLE_FOLIAGE_DATA_SIZE_BYTES); - //construct data texture - Texture dataTexture = new Texture(Globals.renderingEngine.getOpenGLState(),buffer,SINGLE_FOLIAGE_DATA_SIZE_BYTES / 4,FINAL_SPACING * FINAL_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); - } - } - } - - /** - * Creates a foliage chunk - * @param worldPosition The world position of the chunk - */ - private void createFoliageChunk(Vector3i worldPosition){ - FoliageChunk chunk = new FoliageChunk(worldPosition); - chunkTree.addLeaf(new Vector3d(worldPosition), chunk); - } - - /** - * Evaluates all chunk nodes - * @param chunkNode The node to evaluate - */ - private void evaluateChunks(Vector3d playerPos, OctreeNode chunkNode){ - if(chunkNode.isLeaf()){ - FoliageChunk chunk = chunkNode.getData(); - Vector3d chunkPos = Globals.clientWorldData.convertWorldToRealSpace(chunk.getWorldPos()); - } else { - for(OctreeNode child : chunkNode.getChildren()){ - if(child != null){ - evaluateChunks(playerPos, child); - } - } - } - } - /** * Checks that all dependencies of this manager are ready * @return true if all are ready, false otherwise @@ -556,64 +128,14 @@ public class ClientFoliageManager { } /** - * Gets whether the voxel type supports foliage or not - * @param type - * @return + * Evaluates the foliage chunk at the position + * @param worldPos The position */ - 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(); + public void evaluateChunk(Vector3i worldPos){ + for(FoliageChunk chunk : chunkUpdateCache){ + if(chunk.getWorldPos().equals(worldPos)){ + chunk.updateCells(); + break; } } } @@ -622,8 +144,8 @@ public class ClientFoliageManager { * Draws all foliage in the foliage manager */ public void draw(){ - for(FoliageCell cell : activeCells){ - cell.draw(modelMatrixAttribute); + for(FoliageChunk chunk : chunks){ + chunk.draw(); } } diff --git a/src/main/java/electrosphere/client/foliagemanager/FoliageCell.java b/src/main/java/electrosphere/client/foliagemanager/FoliageCell.java index 19695ea1..16a749da 100644 --- a/src/main/java/electrosphere/client/foliagemanager/FoliageCell.java +++ b/src/main/java/electrosphere/client/foliagemanager/FoliageCell.java @@ -1,6 +1,13 @@ 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.List; +import java.util.Map; +import java.util.Random; import java.util.Set; import org.joml.Matrix4d; @@ -9,20 +16,114 @@ import org.joml.Sphered; 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.entity.types.camera.CameraEntityUtils; +import electrosphere.game.data.foliage.type.FoliageType; import electrosphere.renderer.OpenGLState; import electrosphere.renderer.RenderPipelineState; import electrosphere.renderer.actor.instance.TextureInstancedActor; +import electrosphere.renderer.buffer.HomogenousUniformBuffer.HomogenousBufferTypes; import electrosphere.renderer.buffer.ShaderAttribute; +import electrosphere.renderer.texture.Texture; +import electrosphere.server.terrain.manager.ServerTerrainChunk; /** * Contains a set of foliage entities and groups them together. */ public class FoliageCell { + + /** + * 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; + + /** + * 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; + + /** + *

+ * 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; + + /** + * The map of all attributes for instanced foliage + */ + static final Map attributes = new HashMap(); + + /** + * model matrix shader attribute + */ + static ShaderAttribute modelMatrixAttribute; + + /** + * The list of voxel type ids that should have grass generated on top of them + */ + static final List grassGeneratingVoxelIds = new ArrayList(); + + //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); + } + + /** + * Vertex shader path + */ + static final String vertexPath = "Shaders/entities/foliage/foliage.vs"; + + /** + * fragment shader path + */ + static final String fragmentPath = "Shaders/entities/foliage/foliage.fs"; + + /** + * Random for finding new positions for foliage + */ + Random placementRandomizer = new Random(); + + /** + * The scale of the chunk + */ + int scale; + //position of the foliage cell in world coordinates protected Vector3i worldPosition; //position of the foliage cell in local coordinates @@ -40,15 +141,29 @@ public class FoliageCell { */ protected float density; + /** + * Inits the foliage cell data + */ + static void init(){ + //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); + } + } + } + /** * Constructor * @param worldPos The position of the foliage cell in world coordinates * @param voxelPos The position of the foliage cell in voxel coordinates */ - protected FoliageCell(Vector3i worldPos, Vector3i voxelPos, Vector3d realPos, float density){ + protected FoliageCell(Vector3i worldPos, Vector3i voxelPos, Vector3d realPos, int scale, float density){ this.worldPosition = worldPos; this.voxelPosition = voxelPos; this.realPosition = realPos; + this.scale = scale; this.density = density; this.containedEntities = new HashSet(); } @@ -79,41 +194,340 @@ public class FoliageCell { } /** - * Draws all entities in the foliage cell - * @param modelMatrixAttribute The model matrix attribute to draw with + * Generates the foliage cell */ - protected void draw(ShaderAttribute modelMatrixAttribute){ - Matrix4d modelMatrix = new Matrix4d(); - Vector3f cameraCenter = CameraEntityUtils.getCameraCenter(Globals.playerCamera); - - RenderPipelineState renderPipelineState = Globals.renderingEngine.getRenderPipelineState(); - OpenGLState openGLState = Globals.renderingEngine.getOpenGLState(); - - Vector3f cameraModifiedPosition = new Vector3f((float)realPosition.x,(float)realPosition.y,(float)realPosition.z).sub(CameraEntityUtils.getCameraCenter(Globals.playerCamera)); - //frustum check entire cell - boolean shouldRender = renderPipelineState.getFrustumIntersection().testSphere((float)(cameraModifiedPosition.x + boundingSphere.x), (float)(cameraModifiedPosition.y + boundingSphere.y), (float)(cameraModifiedPosition.z + boundingSphere.z), (float)(boundingSphere.r)); - if(shouldRender){ - //disable frustum check and instead perform at cell level - boolean currentFrustumCheckState = renderPipelineState.shouldFrustumCheck(); - renderPipelineState.setFrustumCheck(false); - for(Entity entity : containedEntities){ - Vector3d grassPosition = EntityUtils.getPosition(entity); - Quaterniond grassRotation = EntityUtils.getRotation(entity); - TextureInstancedActor actor = TextureInstancedActor.getTextureInstancedActor(entity); - - - modelMatrix = modelMatrix.identity(); - cameraModifiedPosition = new Vector3f((float)grassPosition.x,(float)grassPosition.y,(float)grassPosition.z).sub(cameraCenter); - modelMatrix.translate(cameraModifiedPosition); - modelMatrix.rotate(new Quaterniond(grassRotation)); - modelMatrix.scale(new Vector3d(EntityUtils.getScale(entity))); - actor.applySpatialData(modelMatrix,grassPosition); - - - //draw - actor.draw(renderPipelineState, openGLState); + protected void generate(){ + boolean shouldGenerate = false; + //get foliage types supported + ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPosition); + List foliageTypesSupported = null; + if(data != null && voxelPosition.y + 1 < ServerTerrainChunk.CHUNK_DIMENSION){ + foliageTypesSupported = Globals.gameConfigCurrent.getVoxelData().getTypeFromId(data.getType(voxelPosition)).getAmbientFoliage(); + boolean airAbove = data.getType(voxelPosition.x,voxelPosition.y+1,voxelPosition.z) == 0; + if(foliageTypesSupported != null && airAbove && scale < 2){ + shouldGenerate = true; + } + } + if(shouldGenerate){ + + //get type + String foliageTypeName = foliageTypesSupported.get(placementRandomizer.nextInt() % foliageTypesSupported.size()); + FoliageType foliageType = Globals.gameConfigCurrent.getFoliageMap().getFoliage(foliageTypeName); + + //create cell and buffer + int FINAL_SPACING = (int)(TARGET_FOLIAGE_SPACING * density); + ByteBuffer buffer = BufferUtils.createByteBuffer(FINAL_SPACING * FINAL_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(realPosition).add(-0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_01 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).add(-0.5,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_02 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).add(-0.5,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_10 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).add( 0,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_11 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).add( 0,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_12 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).add( 0,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_20 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).add( 0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_21 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).add( 0.5,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_22 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).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 < FINAL_SPACING; x++){ + for(int z = 0; z < FINAL_SPACING; z++){ + //get position to place + double rand1 = placementRandomizer.nextDouble(); + double rand2 = placementRandomizer.nextDouble(); + double relativePositionOnGridX = x / (1.0 * FINAL_SPACING) + rand1 / FINAL_SPACING; + double relativePositionOnGridZ = z / (1.0 * FINAL_SPACING) + rand2 / FINAL_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 - realPosition.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(FINAL_SPACING * FINAL_SPACING * SINGLE_FOLIAGE_DATA_SIZE_BYTES); + //construct data texture + Texture dataTexture = new Texture(Globals.renderingEngine.getOpenGLState(),buffer,SINGLE_FOLIAGE_DATA_SIZE_BYTES / 4,FINAL_SPACING * FINAL_SPACING); + + //create entity + Entity grassEntity = EntityCreationUtils.createClientSpatialEntity(); + + TextureInstancedActor.attachTextureInstancedActor(grassEntity, foliageType.getModelPath(), vertexPath, fragmentPath, dataTexture, drawCount); + EntityUtils.getPosition(grassEntity).set(realPosition); + EntityUtils.getRotation(grassEntity).set(0,0,0,1); + EntityUtils.getScale(grassEntity).set(1,1,1); + //add ambient foliage behavior tree + AmbientFoliage.attachAmbientFoliageTree(grassEntity, 0.0f, foliageType.getGrowthModel().getGrowthRate()); + this.addEntity(grassEntity); } - renderPipelineState.setFrustumCheck(currentFrustumCheckState); } } + + /** + * 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(); + } + + /** + * 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); + } + + /** + * Draws all entities in the foliage cell + */ + protected void draw(){ + if(this.containedEntities.size() > 0){ + Matrix4d modelMatrix = new Matrix4d(); + Vector3f cameraCenter = CameraEntityUtils.getCameraCenter(Globals.playerCamera); + + RenderPipelineState renderPipelineState = Globals.renderingEngine.getRenderPipelineState(); + OpenGLState openGLState = Globals.renderingEngine.getOpenGLState(); + + Vector3f cameraModifiedPosition = new Vector3f((float)realPosition.x,(float)realPosition.y,(float)realPosition.z).sub(CameraEntityUtils.getCameraCenter(Globals.playerCamera)); + //frustum check entire cell + boolean shouldRender = true;//renderPipelineState.getFrustumIntersection().testSphere((float)(cameraModifiedPosition.x + boundingSphere.x), (float)(cameraModifiedPosition.y + boundingSphere.y), (float)(cameraModifiedPosition.z + boundingSphere.z), (float)(boundingSphere.r)); + if(shouldRender){ + //disable frustum check and instead perform at cell level + boolean currentFrustumCheckState = renderPipelineState.shouldFrustumCheck(); + renderPipelineState.setFrustumCheck(false); + for(Entity entity : containedEntities){ + Vector3d grassPosition = EntityUtils.getPosition(entity); + Quaterniond grassRotation = EntityUtils.getRotation(entity); + TextureInstancedActor actor = TextureInstancedActor.getTextureInstancedActor(entity); + + + modelMatrix = modelMatrix.identity(); + cameraModifiedPosition = new Vector3f((float)grassPosition.x,(float)grassPosition.y,(float)grassPosition.z).sub(cameraCenter); + modelMatrix.translate(cameraModifiedPosition); + modelMatrix.rotate(new Quaterniond(grassRotation)); + modelMatrix.scale(new Vector3d(EntityUtils.getScale(entity))); + actor.applySpatialData(modelMatrix,grassPosition); + + + //draw + actor.draw(renderPipelineState, openGLState); + } + renderPipelineState.setFrustumCheck(currentFrustumCheckState); + } + } + } + + + /** + * SCAFFOLDING FOR BUILDING SCALE>1 CELLS AND ALSO FOR TOP LEVEL CELL CHECKING + */ + /** + * 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,targetDensity); + // } + // } + // } + // } + // } + // //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,targetDensity); + // } + // } + // } + // } + // } + // } + + /** + * 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; + } } diff --git a/src/main/java/electrosphere/client/foliagemanager/FoliageChunk.java b/src/main/java/electrosphere/client/foliagemanager/FoliageChunk.java index bd80c099..a7e94614 100644 --- a/src/main/java/electrosphere/client/foliagemanager/FoliageChunk.java +++ b/src/main/java/electrosphere/client/foliagemanager/FoliageChunk.java @@ -1,10 +1,17 @@ package electrosphere.client.foliagemanager; -import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; +import org.joml.Vector3d; import org.joml.Vector3i; +import electrosphere.client.terrain.cache.ChunkData; +import electrosphere.engine.Globals; +import electrosphere.entity.EntityUtils; +import electrosphere.util.ds.octree.ChunkTree; +import electrosphere.util.ds.octree.ChunkTree.ChunkTreeNode; + /** * A whole chunk of foliage */ @@ -14,11 +21,27 @@ public class FoliageChunk { * The world position of this chunk */ Vector3i worldPos; + Vector3d realPos; /** - * A list of all cells that are currently generated + * The distance to draw at full resolution */ - List currentlyGeneratedCellPositions; + static final double FULL_RES_DIST = 15; + + /** + * The octree holding all the chunks to evaluate + */ + ChunkTree chunkTree; + + /** + * Data for the current chunk + */ + ChunkData currentChunkData; + + /** + * Data for the above chunk + */ + ChunkData aboveChunkData; /** * Constructor @@ -26,7 +49,8 @@ public class FoliageChunk { */ public FoliageChunk(Vector3i worldPos){ this.worldPos = worldPos; - this.currentlyGeneratedCellPositions = new ArrayList(); + this.realPos = Globals.clientWorldData.convertWorldToRealSpace(worldPos); + this.chunkTree = new ChunkTree(); } /** @@ -37,4 +61,178 @@ public class FoliageChunk { return worldPos; } + /** + * Initializes all cells in the chunk + */ + public void initCells(){ + Vector3d playerPos = EntityUtils.getPosition(Globals.playerEntity); + this.currentChunkData = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos); + // //evaluate top cells if chunk above this one exists + this.aboveChunkData = Globals.clientTerrainManager.getChunkDataAtWorldPoint(new Vector3i(worldPos).add(0,1,0)); + + //the sets to iterate through + ChunkTreeNode rootNode = this.chunkTree.getRoot(); + List> openSet = new LinkedList>(); + List> toInit = new LinkedList>(); + openSet.add(rootNode); + + //split into nodes + while(openSet.size() > 0){ + ChunkTreeNode current = openSet.remove(0); + if(this.shouldSplit(playerPos, current)){ + //add children for this one + ChunkTreeNode container = chunkTree.split(current); + openSet.addAll(container.getChildren()); + } else { + //do nothing + toInit.add(current); + } + } + //init end-nodes as leaves + for(ChunkTreeNode current : toInit){ + Vector3d realPos = Globals.clientWorldData.convertWorldToRealSpace(worldPos).add(new Vector3d(current.getMinBound())); + current.convertToLeaf(new FoliageCell(worldPos, current.getMinBound(), realPos, 4 - current.getLevel(), 1.0f)); + current.getData().generate(); + } + } + + /** + * Updates all cells in the chunk + */ + public void updateCells(){ + Vector3d playerPos = EntityUtils.getPosition(Globals.playerEntity); + //the sets to iterate through + ChunkTreeNode rootNode = this.chunkTree.getRoot(); + List> openSet = new LinkedList>(); + List> toInit = new LinkedList>(); + List> toCleanup = new LinkedList>(); + openSet.add(rootNode); + //split into nodes + while(openSet.size() > 0){ + ChunkTreeNode current = openSet.remove(0); + + if(this.shouldSplit(playerPos, current)){ + //add children for this one + ChunkTreeNode container = chunkTree.split(current); + toInit.addAll(container.getChildren()); + toCleanup.add(current); + } else if(this.shouldJoin(playerPos, current)) { + //add children for this one + ChunkTreeNode newLeaf = chunkTree.join(current); + toCleanup.addAll(current.getChildren()); + toCleanup.add(current); + toInit.add(newLeaf); + } else if(!current.isLeaf()){ + openSet.addAll(current.getChildren()); + } + } + //init end-nodes as leaves + for(ChunkTreeNode current : toInit){ + Vector3d realPos = Globals.clientWorldData.convertWorldToRealSpace(worldPos).add(new Vector3d(current.getMinBound())); + current.convertToLeaf(new FoliageCell(worldPos, current.getMinBound(), realPos, 5 - current.getLevel(), 1.0f)); + current.getData().generate(); + } + //destroy orphan nodes + for(ChunkTreeNode current : toCleanup){ + if(current.getData() != null){ + current.getData().destroy(); + } + } + + } + + /** + * Gets the minimum distance from a node to a point + * @param pos the position to check against + * @param node the node + * @return the distance + */ + public double getMinDistance(Vector3d pos, ChunkTreeNode node){ + Vector3i min = node.getMinBound(); + Vector3i max = node.getMaxBound(); + double minX = min.x + realPos.x; + double minY = min.y + realPos.y; + double minZ = min.z + realPos.z; + double maxX = max.x + realPos.x; + double maxY = max.y + realPos.y; + double maxZ = max.z + realPos.z; + if(Math.abs(pos.x - minX) < Math.abs(pos.x - maxX)){ + if(Math.abs(pos.y - minY) < Math.abs(pos.y - maxY)){ + if(Math.abs(pos.z - minZ) < Math.abs(pos.z - maxZ)){ + return pos.distance(minX,minY,minZ); + } else { + return pos.distance(minX,minY,maxZ); + } + } else { + if(Math.abs(pos.z - minZ) < Math.abs(pos.z - maxZ)){ + return pos.distance(minX,maxY,minZ); + } else { + return pos.distance(minX,maxY,maxZ); + } + } + } else { + if(Math.abs(pos.y - minY) < Math.abs(pos.y - maxY)){ + if(Math.abs(pos.z - minZ) < Math.abs(pos.z - maxZ)){ + return pos.distance(maxX,minY,minZ); + } else { + return pos.distance(maxX,minY,maxZ); + } + } else { + if(Math.abs(pos.z - minZ) < Math.abs(pos.z - maxZ)){ + return pos.distance(maxX,maxY,minZ); + } else { + return pos.distance(maxX,maxY,maxZ); + } + } + } + } + + /** + * Gets whether this should be split or not + * @param pos the player position + * @param node The node + * @return true if should split, false otherwise + */ + public boolean shouldSplit(Vector3d pos, ChunkTreeNode node){ + //breaking out into dedicated function so can add case handling ie if we want + //to combine fullres nodes into larger nodes to conserve on draw calls + return this.getMinDistance(pos, node) <= FULL_RES_DIST && + node.isLeaf() && + node.canSplit() + ; + } + + /** + * Gets whether this should be joined or not + * @param pos the player position + * @param node The node + * @return true if should be joined, false otherwise + */ + public boolean shouldJoin(Vector3d pos, ChunkTreeNode node){ + //breaking out into dedicated function so can add case handling ie if we want + //to combine fullres nodes into larger nodes to conserve on draw calls + return this.getMinDistance(pos, node) > FULL_RES_DIST && + !node.isLeaf() + ; + } + + /** + * Destroys the foliage chunk + */ + protected void destroy(){ + for(ChunkTreeNode leaf : this.chunkTree.getLeaves()){ + leaf.getData().destroy(); + } + } + + /** + * Draws all cells in the chunk + */ + protected void draw(){ + for(ChunkTreeNode leaf : this.chunkTree.getLeaves()){ + leaf.getData().draw(); + } + } + + } diff --git a/src/main/java/electrosphere/util/ds/octree/ChunkTree.java b/src/main/java/electrosphere/util/ds/octree/ChunkTree.java new file mode 100644 index 00000000..b133d9d6 --- /dev/null +++ b/src/main/java/electrosphere/util/ds/octree/ChunkTree.java @@ -0,0 +1,284 @@ +package electrosphere.util.ds.octree; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.stream.Collectors; + +import org.joml.Vector3i; + +/** + * An octree that has a guaranteed subdivision strategy that makes it appealing to use for chunk-based operations + */ +public class ChunkTree { + + /** + * The dimension of the tree + */ + static final int DIMENSION = 16; + + /** + * The maximum level + */ + public static final int MAX_LEVEL = 4; + + //The root node + private ChunkTreeNode root = null; + + /** + * The list of all nodes in the tree + */ + List> nodes = null; + + /** + * Constructor + */ + public ChunkTree(){ + this.nodes = new ArrayList>(); + this.root = new ChunkTreeNode(0, new Vector3i(0,0,0), new Vector3i(16,16,16)); + this.root.isLeaf = true; + this.nodes.add(this.root); + } + + /** + * Splits a parent into child nodes + * @param parent The parent + * @return The new non-leaf node + */ + public ChunkTreeNode split(ChunkTreeNode existing){ + if(!existing.isLeaf()){ + throw new IllegalArgumentException("Tried to split non-leaf!"); + } + Vector3i min = existing.getMinBound(); + Vector3i max = existing.getMaxBound(); + int midX = (max.x - min.x) / 2 + min.x; + int midY = (max.y - min.y) / 2 + min.y; + int midZ = (max.z - min.z) / 2 + min.z; + int currentLevel = existing.getLevel(); + ChunkTreeNode newContainer = new ChunkTreeNode<>(currentLevel, min, max); + //add children + newContainer.addChild(new ChunkTreeNode(currentLevel + 1, new Vector3i(min.x,min.y,min.z), new Vector3i(midX,midY,midZ))); + newContainer.addChild(new ChunkTreeNode(currentLevel + 1, new Vector3i(midX,min.y,min.z), new Vector3i(max.x,midY,midZ))); + newContainer.addChild(new ChunkTreeNode(currentLevel + 1, new Vector3i(min.x,midY,min.z), new Vector3i(midX,max.y,midZ))); + newContainer.addChild(new ChunkTreeNode(currentLevel + 1, new Vector3i(midX,midY,min.z), new Vector3i(max.x,max.y,midZ))); + // + newContainer.addChild(new ChunkTreeNode(currentLevel + 1, new Vector3i(min.x,min.y,midZ), new Vector3i(midX,midY,max.z))); + newContainer.addChild(new ChunkTreeNode(currentLevel + 1, new Vector3i(midX,min.y,midZ), new Vector3i(max.x,midY,max.z))); + newContainer.addChild(new ChunkTreeNode(currentLevel + 1, new Vector3i(min.x,midY,midZ), new Vector3i(midX,max.y,max.z))); + newContainer.addChild(new ChunkTreeNode(currentLevel + 1, new Vector3i(midX,midY,midZ), new Vector3i(max.x,max.y,max.z))); + + //replace existing node + replaceNode(existing,newContainer); + + //update tracking + this.nodes.remove(existing); + this.nodes.add(newContainer); + this.nodes.addAll(newContainer.getChildren()); + + return newContainer; + } + + /** + * Joins a non-leaf node's children into a single node + * @param parent The non-leaf + * @return The new leaf node + */ + public ChunkTreeNode join(ChunkTreeNode existing){ + if(existing.isLeaf()){ + throw new IllegalArgumentException("Tried to split non-leaf!"); + } + Vector3i min = existing.getMinBound(); + Vector3i max = existing.getMaxBound(); + int currentLevel = existing.getLevel(); + ChunkTreeNode newContainer = new ChunkTreeNode<>(currentLevel, min, max); + + //replace existing node + replaceNode(existing,newContainer); + + //update tracking + this.nodes.remove(existing); + this.nodes.removeAll(existing.getChildren()); + this.nodes.add(newContainer); + + return newContainer; + } + + /** + * Replaces an existing node with a new node + * @param existing the existing node + * @param newNode the new node + */ + private void replaceNode(ChunkTreeNode existing, ChunkTreeNode newNode){ + if(existing == this.root){ + this.root = newNode; + } else { + ChunkTreeNode parent = existing.getParent(); + parent.removeChild(existing); + parent.addChild(newNode); + } + } + + /** + * Gets the root node of the tree + */ + public ChunkTreeNode getRoot() { + return this.root; + } + + /** + * Gets the number of leaves in the tree + */ + public int getNumLeaves() { + return this.getLeaves().size(); + } + + /** + * Gets all leaf nodes + * @return All leaf nodes + */ + public List> getLeaves(){ + return this.nodes.stream().filter(node -> node.isLeaf()).collect(Collectors.toList()); + } + + + /** + * A node in a chunk tree + */ + public static class ChunkTreeNode { + + //True if this is a leaf node, false otherwise + private boolean isLeaf; + + //the parent node + private ChunkTreeNode parent; + + //the children of this node + private List> children = new LinkedList>(); + + //The data at the node + private T data; + + /** + * The min bound + */ + private Vector3i min; + + /** + * The max bound + */ + private Vector3i max; + + /** + * The level of the chunk tree node + */ + int level; + + /** + * Constructor for non-leaf node + */ + private ChunkTreeNode(int level, Vector3i min, Vector3i max){ + if(min.x == 16 || min.y == 16 || min.z == 16){ + throw new IllegalArgumentException("Invalid minimum! " + min); + } + if(level < 0 || level > MAX_LEVEL){ + throw new IllegalArgumentException("Invalid level! " + level); + } + this.isLeaf = false; + this.level = level; + this.min = min; + this.max = max; + } + + /** + * Converts this node to a leaf + * @param data The data to put in the leaf + */ + public void convertToLeaf(T data){ + this.isLeaf = true; + this.data = data; + } + + /** + * Gets the data associated with this node + */ + public T getData() { + return data; + } + + /** + * Gets the parent of this node + */ + public ChunkTreeNode getParent() { + return parent; + } + + /** + * Gets the children of this node + */ + public List> getChildren() { + return Collections.unmodifiableList(this.children); + } + + /** + * Checks if this node is a leaf + * @return true if it is a leaf, false otherwise + */ + public boolean isLeaf() { + return isLeaf; + } + + /** + * Checks if the node can split + * @return true if can split, false otherwise + */ + public boolean canSplit(){ + return isLeaf && level < MAX_LEVEL; + } + + /** + * Gets the level of the node + * @return The level of the node + */ + public int getLevel(){ + return level; + } + + /** + * Gets the min bound of this node + * @return The min bound + */ + public Vector3i getMinBound(){ + return min; + } + + /** + * Gets the max bound of this node + * @return The max bound + */ + public Vector3i getMaxBound(){ + return max; + } + + /** + * Adds a child to this node + * @param child The child + */ + private void addChild(ChunkTreeNode child){ + this.children.add(child); + child.parent = this; + } + + /** + * Removes a child node + * @param child the child + */ + private void removeChild(ChunkTreeNode child){ + this.children.remove(child); + child.parent = null; + } + + } + + +} diff --git a/src/main/java/electrosphere/util/ds/octree/Octree.java b/src/main/java/electrosphere/util/ds/octree/Octree.java new file mode 100644 index 00000000..feeeedfe --- /dev/null +++ b/src/main/java/electrosphere/util/ds/octree/Octree.java @@ -0,0 +1,51 @@ +package electrosphere.util.ds.octree; + +import org.joml.Vector3d; + +/** + * An octree implementation + */ +public interface Octree { + + /** + * Adds a leaf to the octree + * @param location The location of the leaf + * @param data The data associated with the leaf + * @throws IllegalArgumentException Thrown if a location is provided that already has an exact match + */ + public void addLeaf(Vector3d location, T data) throws IllegalArgumentException; + + /** + * Checks if the octree contains a leaf at an exact location + * @param location The location + * @return true if a leaf exists for that exact location, false otherwise + */ + public boolean containsLeaf(Vector3d location); + + /** + * Gets the leaf at an exact location + * @param location The location + * @return The leaf + * @throws ArrayIndexOutOfBoundsException Thrown if a location is queries that does not have an associated leaf in the octree + */ + public OctreeNode getLeaf(Vector3d location) throws ArrayIndexOutOfBoundsException; + + /** + * Removes a node from the octree + * @param node The node + */ + public void removeNode(OctreeNode node); + + /** + * Gets the root node + * @return The root node + */ + public OctreeNode getRoot(); + + /** + * Gets the number of leaves under this tree + * @return The number of leaves + */ + public int getNumLeaves(); + +} diff --git a/src/main/java/electrosphere/util/ds/octree/OctreeNode.java b/src/main/java/electrosphere/util/ds/octree/OctreeNode.java new file mode 100644 index 00000000..eaaec931 --- /dev/null +++ b/src/main/java/electrosphere/util/ds/octree/OctreeNode.java @@ -0,0 +1,34 @@ +package electrosphere.util.ds.octree; + +import java.util.List; + +/** + * An octree node + */ +public interface OctreeNode { + + /** + * Gets the data in the node + * @return The data + */ + public T getData(); + + /** + * Gets the parent node of this node + * @return The parent node + */ + public OctreeNode getParent(); + + /** + * Gets the children of this node + * @return The children + */ + public List> getChildren(); + + /** + * Checks if this is a leaf node or not + * @return true if it is a leaf, false otherwise + */ + public boolean isLeaf(); + +} diff --git a/src/main/java/electrosphere/util/ds/Octree.java b/src/main/java/electrosphere/util/ds/octree/RealOctree.java similarity index 90% rename from src/main/java/electrosphere/util/ds/Octree.java rename to src/main/java/electrosphere/util/ds/octree/RealOctree.java index 9180ab9a..b15b4afe 100644 --- a/src/main/java/electrosphere/util/ds/Octree.java +++ b/src/main/java/electrosphere/util/ds/octree/RealOctree.java @@ -1,4 +1,4 @@ -package electrosphere.util.ds; +package electrosphere.util.ds.octree; import java.util.HashMap; import java.util.LinkedList; @@ -11,7 +11,7 @@ import org.joml.Vector3d; /** * An octree implementation */ -public class Octree { +public class RealOctree { /** * The number of children in a full, non-leaf node @@ -20,20 +20,20 @@ public class Octree { //The root node - private OctreeNode root = null; + private RealOctreeNode root = null; /** * Table used for looking up the presence of nodes * Maps a position key to a corresponding Octree node */ - Map> lookupTable = new HashMap>(); + Map> lookupTable = new HashMap>(); /** * Creates an octree * @param center The center of the root node of the octree */ - public Octree(Vector3d boundLower, Vector3d boundUpper){ - root = new OctreeNode(boundLower, boundUpper); + public RealOctree(Vector3d boundLower, Vector3d boundUpper){ + root = new RealOctreeNode(boundLower, boundUpper); } /** @@ -47,18 +47,18 @@ public class Octree { if(this.containsLeaf(location)){ throw new IllegalArgumentException("Tried adding leaf that is already occupied!"); } - OctreeNode node = new OctreeNode(data, location); - OctreeNode current = this.root; + RealOctreeNode node = new RealOctreeNode(data, location); + RealOctreeNode current = this.root; while(current.children.size() == FULL_NODE_SIZE){ if(current.hasChildInQuadrant(location)){ - OctreeNode child = current.getChildInQuadrant(location); + RealOctreeNode child = current.getChildInQuadrant(location); if(child.isLeaf()){ //get parent of the new fork - OctreeNode parent = child.parent; + RealOctreeNode parent = child.parent; parent.removeChild(child); //while the newly forked child isn't between the two children, keep forking - OctreeNode nonLeaf = parent.forkQuadrant(child.getLocation()); + RealOctreeNode nonLeaf = parent.forkQuadrant(child.getLocation()); while(nonLeaf.getQuadrant(child.getLocation()) == nonLeaf.getQuadrant(node.getLocation())){ nonLeaf = nonLeaf.forkQuadrant(child.getLocation()); } @@ -89,13 +89,15 @@ public class Octree { return lookupTable.containsKey(this.getLocationKey(location)); } + + /** * Gets the leaf at an exact location * @param location The location * @return The leaf * @throws ArrayIndexOutOfBoundsException Thrown if a location is queries that does not have an associated leaf in the octree */ - public OctreeNode getLeaf(Vector3d location) throws ArrayIndexOutOfBoundsException { + public RealOctreeNode getLeaf(Vector3d location) throws ArrayIndexOutOfBoundsException { if(!this.containsLeaf(location)){ throw new ArrayIndexOutOfBoundsException("Tried to get leaf at position that does not contain leaf!"); } @@ -106,8 +108,8 @@ public class Octree { * Removes a node from the octree * @param node The node */ - public void removeNode(OctreeNode node){ - OctreeNode parent = node.parent; + public void removeNode(RealOctreeNode node){ + RealOctreeNode parent = node.parent; parent.removeChild(node); if(node.isLeaf()){ this.lookupTable.remove(this.getLocationKey(node.getLocation())); @@ -118,7 +120,7 @@ public class Octree { * Gets the root node * @return The root node */ - public OctreeNode getRoot(){ + public RealOctreeNode getRoot(){ return this.root; } @@ -143,7 +145,7 @@ public class Octree { /** * A single node in the octree */ - public static class OctreeNode { + public static class RealOctreeNode implements OctreeNode { /* 6 +----------+ 7 @@ -178,10 +180,10 @@ public class Octree { private boolean isLeaf; //the parent node - private OctreeNode parent; + private RealOctreeNode parent; //the children of this node - private List> children = new LinkedList>(); + private List> children = new LinkedList>(); //The data at the node private T data; @@ -191,7 +193,7 @@ public class Octree { * @param data The data at this octree node's position * @param location The location of the node */ - private OctreeNode(T data, Vector3d location){ + private RealOctreeNode(T data, Vector3d location){ this.data = data; this.midpoint = location; this.isLeaf = true; @@ -202,7 +204,7 @@ public class Octree { * @param lowerBound The lower bound of the node * @param upperBound The upper bound of the node */ - private OctreeNode(Vector3d lowerBound, Vector3d upperBound){ + private RealOctreeNode(Vector3d lowerBound, Vector3d upperBound){ this.data = null; this.midpoint = new Vector3d( ((upperBound.x - lowerBound.x) / 2.0) + lowerBound.x, @@ -221,7 +223,7 @@ public class Octree { * Creates a non-leaf node * @param midpoint The midpoint of the node */ - private OctreeNode(Vector3d midpoint){ + private RealOctreeNode(Vector3d midpoint){ this.data = null; this.midpoint = midpoint; for(int i = 0; i < FULL_NODE_SIZE; i++){ @@ -250,7 +252,7 @@ public class Octree { * Gets the parent node of this node * @return The parent node */ - public OctreeNode getParent(){ + public RealOctreeNode getParent(){ return parent; } @@ -269,7 +271,7 @@ public class Octree { */ public boolean hasChildInQuadrant(Vector3d positionToCheck){ int positionQuadrant = this.getQuadrant(positionToCheck); - OctreeNode child = this.children.get(positionQuadrant); + RealOctreeNode child = this.children.get(positionQuadrant); return child != null; } @@ -278,9 +280,9 @@ public class Octree { * @param positionToQuery The position of the quadrant * @return The child if it exists, null otherwise */ - public OctreeNode getChildInQuadrant(Vector3d positionToQuery){ + public RealOctreeNode getChildInQuadrant(Vector3d positionToQuery){ int positionQuadrant = this.getQuadrant(positionToQuery); - OctreeNode child = this.children.get(positionQuadrant); + RealOctreeNode child = this.children.get(positionQuadrant); return child; } @@ -298,7 +300,7 @@ public class Octree { */ public int getNumChildren(){ int acc = 0; - for(OctreeNode child : children){ + for(RealOctreeNode child : children){ if(child != null){ acc++; } @@ -315,7 +317,7 @@ public class Octree { if(this.isLeaf()){ return 1; } else { - for(OctreeNode child : this.children){ + for(RealOctreeNode child : this.children){ if(child != null){ if(child.isLeaf()){ acc++; @@ -332,7 +334,7 @@ public class Octree { * Adds a child to this node * @param child The child */ - private void addChild(OctreeNode child){ + private void addChild(RealOctreeNode child){ if(hasChildInQuadrant(child.midpoint)){ throw new IllegalArgumentException("Trying to add child in occupied quadrant!"); } @@ -346,7 +348,7 @@ public class Octree { * Removes a child from this node * @param child The child */ - private void removeChild(OctreeNode child){ + private void removeChild(RealOctreeNode child){ if(child == null){ throw new IllegalArgumentException("Child cannot be null!"); } @@ -396,7 +398,7 @@ public class Octree { * Sets the bounds of the child based on the quadrant it falls under * @param child The child */ - private void setChildBounds(OctreeNode child){ + private void setChildBounds(RealOctreeNode child){ if(child.midpoint.z < this.midpoint.z){ if(child.midpoint.y < this.midpoint.y){ if(child.midpoint.x < this.midpoint.x){ @@ -580,11 +582,11 @@ public class Octree { * @param location The location within the quadrant * @return The midpoint of the new node */ - private OctreeNode forkQuadrant(Vector3d location){ + private RealOctreeNode forkQuadrant(Vector3d location){ int quadrant = this.getQuadrant(location); Vector3d midpoint = this.getQuadrantMidpoint(quadrant); //create and add the non-leaf node - OctreeNode nonLeaf = new OctreeNode(midpoint); + RealOctreeNode nonLeaf = new RealOctreeNode(midpoint); this.addChild(nonLeaf); return nonLeaf; } diff --git a/src/test/java/electrosphere/util/ds/OctreeTests.java b/src/test/java/electrosphere/util/ds/octree/RealOctreeTests.java similarity index 70% rename from src/test/java/electrosphere/util/ds/OctreeTests.java rename to src/test/java/electrosphere/util/ds/octree/RealOctreeTests.java index b062c559..e4616cf2 100644 --- a/src/test/java/electrosphere/util/ds/OctreeTests.java +++ b/src/test/java/electrosphere/util/ds/octree/RealOctreeTests.java @@ -1,4 +1,4 @@ -package electrosphere.util.ds; +package electrosphere.util.ds.octree; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -13,19 +13,19 @@ import java.util.Random; import org.joml.Vector3d; import electrosphere.test.annotations.UnitTest; -import electrosphere.util.ds.Octree.OctreeNode; +import electrosphere.util.ds.octree.RealOctree.RealOctreeNode; /** * Unit testing for the octree implementation */ -public class OctreeTests { +public class RealOctreeTests { /** * Creates an octree */ @UnitTest public void testCreateOctree(){ - new Octree(new Vector3d(0,0,0), new Vector3d(32,32,32)); + new RealOctree(new Vector3d(0,0,0), new Vector3d(32,32,32)); } /** @@ -33,15 +33,15 @@ public class OctreeTests { */ @UnitTest public void testAddNode(){ - Octree octree = new Octree(new Vector3d(0,0,0), new Vector3d(32,32,32)); + RealOctree octree = new RealOctree(new Vector3d(0,0,0), new Vector3d(32,32,32)); octree.addLeaf(new Vector3d(1,1,1), 1); // //validate the tree // - List> openSet = new LinkedList>(); + List> openSet = new LinkedList>(); openSet.add(octree.getRoot()); while(openSet.size() > 0){ - OctreeNode current = openSet.remove(0); + RealOctreeNode current = openSet.remove(0); if(current.isLeaf()){ assertNotNull(current.getData()); } else { @@ -51,7 +51,7 @@ public class OctreeTests { fail("Child not attached to parent!"); } if(child != null){ - openSet.add(child); + openSet.add((RealOctreeNode)child); } } } @@ -63,16 +63,16 @@ public class OctreeTests { */ @UnitTest public void testAddTwo(){ - Octree octree = new Octree(new Vector3d(0,0,0), new Vector3d(32,32,32)); + RealOctree octree = new RealOctree(new Vector3d(0,0,0), new Vector3d(32,32,32)); octree.addLeaf(new Vector3d(1,1,1), 1); octree.addLeaf(new Vector3d(2,1,1), 2); // //validate the tree // - List> openSet = new LinkedList>(); + List> openSet = new LinkedList>(); openSet.add(octree.getRoot()); while(openSet.size() > 0){ - OctreeNode current = openSet.remove(0); + RealOctreeNode current = openSet.remove(0); if(current.isLeaf()){ assertNotNull(current.getData()); } else { @@ -82,7 +82,7 @@ public class OctreeTests { fail("Child not attached to parent!"); } if(child != null){ - openSet.add(child); + openSet.add((RealOctreeNode)child); } } } @@ -90,26 +90,26 @@ public class OctreeTests { // //Specific, expected values to check. Verifies that the octree construct itself correctly // - OctreeNode current = octree.getRoot().getChildren().get(0); + RealOctreeNode current = (RealOctreeNode)octree.getRoot().getChildren().get(0); assertEquals(8, current.getLocation().x); assertEquals(8, current.getLocation().y); assertEquals(8, current.getLocation().z); assertTrue(!current.isLeaf()); - assertEquals(1, current.getNumChildren()); + assertEquals(1, ((RealOctreeNode)current).getNumChildren()); - current = current.getChildren().get(0); + current = (RealOctreeNode)current.getChildren().get(0); assertEquals(4, current.getLocation().x); assertEquals(4, current.getLocation().y); assertEquals(4, current.getLocation().z); assertTrue(!current.isLeaf()); - assertEquals(1, current.getNumChildren()); + assertEquals(1, ((RealOctreeNode)current).getNumChildren()); - current = current.getChildren().get(0); + current = (RealOctreeNode)current.getChildren().get(0); assertEquals(2, current.getLocation().x); assertEquals(2, current.getLocation().y); assertEquals(2, current.getLocation().z); assertTrue(!current.isLeaf()); - assertEquals(2, current.getNumChildren()); + assertEquals(2, ((RealOctreeNode)current).getNumChildren()); OctreeNode leaf1 = current.getChildren().get(0); OctreeNode leaf2 = current.getChildren().get(1); @@ -122,17 +122,17 @@ public class OctreeTests { */ @UnitTest public void testAddLine(){ - Octree octree = new Octree(new Vector3d(0,0,0), new Vector3d(32,32,32)); + RealOctree octree = new RealOctree(new Vector3d(0,0,0), new Vector3d(32,32,32)); for(int i = 0; i < 31; i++){ octree.addLeaf(new Vector3d(i,1,0), i); } // //validate the tree // - List> openSet = new LinkedList>(); + List> openSet = new LinkedList>(); openSet.add(octree.getRoot()); while(openSet.size() > 0){ - OctreeNode current = openSet.remove(0); + RealOctreeNode current = openSet.remove(0); if(current.isLeaf()){ assertNotNull(current.getData()); } else { @@ -142,7 +142,7 @@ public class OctreeTests { fail("Child not attached to parent!"); } if(child != null){ - openSet.add(child); + openSet.add(((RealOctreeNode)child)); } } } @@ -154,17 +154,17 @@ public class OctreeTests { */ @UnitTest public void testGetLeaf(){ - Octree octree = new Octree(new Vector3d(0,0,0), new Vector3d(32,32,32)); + RealOctree octree = new RealOctree(new Vector3d(0,0,0), new Vector3d(32,32,32)); octree.addLeaf(new Vector3d(1,1,1), 1); - OctreeNode leaf = octree.getLeaf(new Vector3d(1,1,1)); + RealOctreeNode leaf = octree.getLeaf(new Vector3d(1,1,1)); assertNotNull(leaf); // //validate the tree // - List> openSet = new LinkedList>(); + List> openSet = new LinkedList>(); openSet.add(octree.getRoot()); while(openSet.size() > 0){ - OctreeNode current = openSet.remove(0); + RealOctreeNode current = openSet.remove(0); if(current.isLeaf()){ assertNotNull(current.getData()); } else { @@ -174,7 +174,7 @@ public class OctreeTests { fail("Child not attached to parent!"); } if(child != null){ - openSet.add(child); + openSet.add((RealOctreeNode)child); } } } @@ -186,19 +186,19 @@ public class OctreeTests { */ @UnitTest public void testRemoveLeaf(){ - Octree octree = new Octree(new Vector3d(0,0,0), new Vector3d(32,32,32)); + RealOctree octree = new RealOctree(new Vector3d(0,0,0), new Vector3d(32,32,32)); octree.addLeaf(new Vector3d(1,1,1), 1); - OctreeNode leaf = octree.getLeaf(new Vector3d(1,1,1)); + RealOctreeNode leaf = octree.getLeaf(new Vector3d(1,1,1)); assertNotNull(leaf); assertNotNull(leaf.getParent()); octree.removeNode(leaf); // //validate the tree // - List> openSet = new LinkedList>(); + List> openSet = new LinkedList>(); openSet.add(octree.getRoot()); while(openSet.size() > 0){ - OctreeNode current = openSet.remove(0); + RealOctreeNode current = openSet.remove(0); if(current.isLeaf()){ assertNotNull(current.getData()); } else { @@ -208,7 +208,7 @@ public class OctreeTests { fail("Child not attached to parent!"); } if(child != null){ - openSet.add(child); + openSet.add((RealOctreeNode)child); } } } @@ -220,11 +220,11 @@ public class OctreeTests { */ @UnitTest public void testRemoveLeaf2(){ - Octree octree = new Octree(new Vector3d(0,0,0), new Vector3d(32,32,32)); + RealOctree octree = new RealOctree(new Vector3d(0,0,0), new Vector3d(32,32,32)); octree.addLeaf(new Vector3d(1,1,1), 1); octree.addLeaf(new Vector3d(2,1,1), 2); octree.addLeaf(new Vector3d(3,1,1), 3); - OctreeNode leaf = octree.getLeaf(new Vector3d(2,1,1)); + RealOctreeNode leaf = octree.getLeaf(new Vector3d(2,1,1)); assertNotNull(leaf); assertNotNull(leaf.getParent()); @@ -242,10 +242,10 @@ public class OctreeTests { // //validate the tree // - List> openSet = new LinkedList>(); + List> openSet = new LinkedList>(); openSet.add(octree.getRoot()); while(openSet.size() > 0){ - OctreeNode current = openSet.remove(0); + RealOctreeNode current = openSet.remove(0); if(current.isLeaf()){ assertNotNull(current.getData()); } else { @@ -255,7 +255,7 @@ public class OctreeTests { fail("Child not attached to parent!"); } if(child != null){ - openSet.add(child); + openSet.add((RealOctreeNode)child); } } } @@ -267,7 +267,7 @@ public class OctreeTests { */ @UnitTest public void testAddManyPoints(){ - Octree octree = new Octree(new Vector3d(0,0,0), new Vector3d(256,256,256)); + RealOctree octree = new RealOctree(new Vector3d(0,0,0), new Vector3d(256,256,256)); //