diff --git a/docs/src/progress/renderertodo.md b/docs/src/progress/renderertodo.md index 6850f10b..75bc2cd7 100644 --- a/docs/src/progress/renderertodo.md +++ b/docs/src/progress/renderertodo.md @@ -1112,6 +1112,7 @@ Remove point lights from skeleton + human Change grass texture Fix allocations on FoliageChunk child iterations Reduce near clip to remove flickering on far chunks +Complete overhaul of foliage management # TODO diff --git a/src/main/java/electrosphere/client/scene/ClientWorldData.java b/src/main/java/electrosphere/client/scene/ClientWorldData.java index 3312ea83..11b5656e 100644 --- a/src/main/java/electrosphere/client/scene/ClientWorldData.java +++ b/src/main/java/electrosphere/client/scene/ClientWorldData.java @@ -115,6 +115,45 @@ public class ClientWorldData { ); } + /** + * Converts a real space position to its absolute voxel space equivalent + * @param position The real space position + * @return The absolute voxel space position ie the voxel-aligned position not clamped to the current chunk + */ + public Vector3i convertRealToAbsoluteVoxelSpace(Vector3d position){ + return new Vector3i( + (int)Math.floor(position.x), + (int)Math.floor(position.y), + (int)Math.floor(position.z) + ); + } + + /** + * Converts a absolute voxel position to its relative voxel space equivalent + * @param position The real space position + * @return The relative voxel space position ie the voxel-aligned position not clamped to the current chunk + */ + public Vector3i convertAbsoluteVoxelToRelativeVoxelSpace(Vector3i position){ + return new Vector3i( + position.x % ServerTerrainChunk.CHUNK_DIMENSION, + position.y % ServerTerrainChunk.CHUNK_DIMENSION, + position.z % ServerTerrainChunk.CHUNK_DIMENSION + ); + } + + /** + * Converts a absolute voxel position to its world space equivalent + * @param position The real space position + * @return The world space position ie the voxel-aligned position not clamped to the current chunk + */ + public Vector3i convertAbsoluteVoxelToWorldSpace(Vector3i position){ + return new Vector3i( + position.x / ServerTerrainChunk.CHUNK_DIMENSION, + position.y / ServerTerrainChunk.CHUNK_DIMENSION, + position.z / ServerTerrainChunk.CHUNK_DIMENSION + ); + } + /** * Converts a world space vector to a real space vector * @param position The world space vector diff --git a/src/main/java/electrosphere/client/sim/ClientSimulation.java b/src/main/java/electrosphere/client/sim/ClientSimulation.java index 1eb8f290..cc53f728 100644 --- a/src/main/java/electrosphere/client/sim/ClientSimulation.java +++ b/src/main/java/electrosphere/client/sim/ClientSimulation.java @@ -87,8 +87,8 @@ public class ClientSimulation { Globals.profiler.endCpuSample(); // //update foliage - if(Globals.clientFoliageManager != null){ - Globals.clientFoliageManager.update(); + if(Globals.foliageCellManager != null){ + Globals.foliageCellManager.update(); } // //targeting crosshair diff --git a/src/main/java/electrosphere/client/terrain/cells/DrawCellManager.java b/src/main/java/electrosphere/client/terrain/cells/DrawCellManager.java index 3ca77405..10db9936 100644 --- a/src/main/java/electrosphere/client/terrain/cells/DrawCellManager.java +++ b/src/main/java/electrosphere/client/terrain/cells/DrawCellManager.java @@ -241,8 +241,8 @@ public class DrawCellManager { List higherLODFace = null; keyCellMap.get(targetKey).generateDrawableEntity(atlas,0,higherLODFace); - //evaluate for foliage - Globals.clientFoliageManager.evaluateChunk(worldPos); + // //evaluate for foliage + // Globals.clientFoliageManager.evaluateChunk(worldPos); } } } diff --git a/src/main/java/electrosphere/client/terrain/foliage/FoliageCell.java b/src/main/java/electrosphere/client/terrain/foliage/FoliageCell.java new file mode 100644 index 00000000..aeb73960 --- /dev/null +++ b/src/main/java/electrosphere/client/terrain/foliage/FoliageCell.java @@ -0,0 +1,701 @@ +package electrosphere.client.terrain.foliage; + +import java.nio.FloatBuffer; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import org.joml.Matrix4d; +import org.joml.Quaterniond; +import org.joml.Vector3d; +import org.joml.Vector3f; +import org.joml.Vector3i; + +import electrosphere.client.entity.camera.CameraEntityUtils; +import electrosphere.client.terrain.cache.ChunkData; +import electrosphere.engine.Globals; +import electrosphere.entity.ClientEntityUtils; +import electrosphere.entity.Entity; +import electrosphere.entity.EntityCreationUtils; +import electrosphere.entity.EntityUtils; +import electrosphere.entity.btree.BehaviorTree; +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.meshgen.TransvoxelModelGeneration.TransvoxelChunkData; +import electrosphere.server.terrain.manager.ServerTerrainChunk; +import electrosphere.util.ds.octree.WorldOctTree.WorldOctTreeNode; +import electrosphere.util.math.GeomUtils; + +/** + * A single foliagecell - contains an entity that has a physics mesh and potentially graphics + */ +public class FoliageCell { + + /** + * Number of frames to wait before destroying the chunk entity + */ + public static final int FRAMES_TO_WAIT_BEFORE_DESTRUCTION = 25; + + /** + * Number of child cells per parent cell + */ + static final int CHILD_CELLS_PER_PARENT = 8; + + /** + * Wiggle room in number of entries + */ + static final int BUFFER_WIGGLE_ROOM = 200; + + /** + * 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 + BUFFER_WIGGLE_ROOM; + + /** + * The length of the ray to ground test with + */ + static final float RAY_LENGTH = 1.0f; + + /** + * The height above the chunk to start from when sampling downwards + */ + static final float SAMPLE_START_HEIGHT = 0.5f; + + /** + * The ID of the air voxel + */ + static final int AIR_VOXEL_ID = 0; + + /** + *

+ * 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 + */ + protected static final String vertexPath = "Shaders/entities/foliage/foliage.vs"; + + /** + * Fragment shader path + */ + protected static final String fragmentPath = "Shaders/entities/foliage/foliage.fs"; + + /** + * Random for finding new positions for foliage + */ + Random placementRandomizer = new Random(); + + + /** + * The position of the foliage cell in world coordinates + */ + Vector3i worldPos; + + /** + * The position of this cell voxel-wise within its chunk + */ + Vector3i voxelPos; + + + /** + * The LOD of the foliage cell + */ + int lod; + + /** + * The main entity for the cell + */ + Entity modelEntity; + + /** + * The data for generating the visuals + */ + TransvoxelChunkData chunkData; + + /** + * Tracks whether the foliage cell has requested its chunk data or not + */ + boolean hasRequested = false; + + /** + * Tracks whether the foliage cell has generated its entity or not + */ + boolean hasGenerated = false; + + + /** + * Tracks whether this foliage cell is flagged as homogenous from the server or not + */ + boolean homogenous = false; + + /** + * Number of failed generation attempts + */ + int failedGenerationAttempts = 0; + + /** + * Labels an invalid distance cache + */ + static final int INVALID_DIST_CACHE = -1; + + /** + * The cached minimum distance + */ + double cachedMinDistance = -1; + + /** + * Target to notify on generation completion + */ + FoliageCell notifyTarget = null; + + /** + * The number of cells that have alerted this one + */ + int generationAlertCount = 0; + + + /** + * Private constructor + */ + private FoliageCell(){ + + } + + + /** + * Constructs a foliagecell object + */ + public static FoliageCell generateTerrainCell( + Vector3i voxelAbsPos, + int lod + ){ + FoliageCell rVal = new FoliageCell(); + rVal.lod = lod; + rVal.worldPos = Globals.clientWorldData.convertAbsoluteVoxelToWorldSpace(voxelAbsPos); + rVal.voxelPos = Globals.clientWorldData.convertAbsoluteVoxelToRelativeVoxelSpace(voxelAbsPos); + return rVal; + } + + /** + * Constructs a homogenous foliagecell object + */ + public static FoliageCell generateHomogenousTerrainCell( + Vector3i voxelAbsPos, + int lod + ){ + FoliageCell rVal = new FoliageCell(); + rVal.lod = lod; + rVal.worldPos = Globals.clientWorldData.convertAbsoluteVoxelToWorldSpace(voxelAbsPos); + rVal.voxelPos = Globals.clientWorldData.convertAbsoluteVoxelToRelativeVoxelSpace(voxelAbsPos); + rVal.hasGenerated = true; + rVal.homogenous = true; + return rVal; + } + + /** + * Generates a drawable entity based on this chunk + */ + public void generateDrawableEntity(int lod){ + boolean success = true; + if(chunkData == null){ + ChunkData currentChunk = Globals.clientTerrainManager.getChunkDataAtWorldPoint( + worldPos.x, + worldPos.y, + worldPos.z, + 0 + ); + if(currentChunk == null){ + success = false; + } else { + this.homogenous = currentChunk.getHomogenousValue() != ChunkData.NOT_HOMOGENOUS; + success = true; + } + if(!success){ + this.setFailedGenerationAttempts(this.getFailedGenerationAttempts() + 1); + return; + } + this.chunkData = new TransvoxelChunkData(currentChunk.getVoxelWeight(), currentChunk.getVoxelType(), 0); + } + this.generate(); + this.setHasGenerated(true); + } + + + /** + * Generates the foliage cell + */ + protected void generate(){ + boolean shouldGenerate = false; + if(voxelPos.y + 1 >= ServerTerrainChunk.CHUNK_DIMENSION){ + return; + } + ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos,ChunkData.NO_STRIDE); + if(data == null){ + return; + } + //get foliage types supported + List foliageTypesSupported = new LinkedList(); + boolean airAbove = data.getType(voxelPos.x,voxelPos.y+1,voxelPos.z) == 0; + int scale = (int)Math.pow(2,lod); + for(int x = 0; x < scale; x++){ + for(int y = 0; y < scale; y++){ + for(int z = 0; z < scale; z++){ + List currentList = Globals.gameConfigCurrent.getVoxelData().getTypeFromId(data.getType(voxelPos)).getAmbientFoliage(); + if(currentList == null){ + continue; + } + foliageTypesSupported.addAll(currentList); + airAbove = data.getType(voxelPos.x,voxelPos.y+1,voxelPos.z) == 0; + if(foliageTypesSupported != null && foliageTypesSupported.size() > 0 && airAbove){ + shouldGenerate = true; + } + } + } + } + if(shouldGenerate){ + //create entity + this.modelEntity = EntityCreationUtils.createClientSpatialEntity(); + FoliageModel.clientCreateFoliageChunkEntity(foliageTypesSupported,scale,this.modelEntity,this.getRealPos(),worldPos,voxelPos,null,null); + //get type + // String foliageTypeName = foliageTypesSupported.get(placementRandomizer.nextInt() % foliageTypesSupported.size()); + // FoliageType foliageType = Globals.gameConfigCurrent.getFoliageMap().getFoliage(foliageTypeName); + + // //create cell and buffer + // ByteBuffer buffer = BufferUtils.createByteBuffer(TARGET_FOLIAGE_PER_CELL * SINGLE_FOLIAGE_DATA_SIZE_BYTES); + // if(buffer.capacity() < TARGET_FOLIAGE_PER_CELL * SINGLE_FOLIAGE_DATA_SIZE_BYTES){ + // LoggerInterface.loggerEngine.WARNING("Failed to allocate data for foliage cell! " + buffer.limit()); + // } + // FloatBuffer floatBufferView = buffer.asFloatBuffer(); + // int drawCount = 0; + // for(int x = 0; x < scale; x++){ + // for(int y = 0; y < scale; y++){ + // for(int z = 0; z < scale; z++){ + // drawCount = drawCount + this.insertBlades(x, y, z, floatBufferView, data); + // } + // } + // } + // // drawCount = drawCount + this.insertBlades(0, 0, 0, floatBufferView, data); + // if(drawCount > 0){ + // buffer.position(0); + // buffer.limit(TARGET_FOLIAGE_PER_CELL * SINGLE_FOLIAGE_DATA_SIZE_BYTES); + // //construct data texture + // Texture dataTexture = new Texture(Globals.renderingEngine.getOpenGLState(),buffer,SINGLE_FOLIAGE_DATA_SIZE_BYTES / 4,TARGET_FOLIAGE_PER_CELL); + + // //create entity + // this.modelEntity = EntityCreationUtils.createClientSpatialEntity(); + + // TextureInstancedActor.attachTextureInstancedActor(this.modelEntity, foliageType.getGraphicsTemplate().getModel().getPath(), vertexPath, fragmentPath, dataTexture, drawCount); + // ClientEntityUtils.initiallyPositionEntity(this.modelEntity, this.getRealPos(), new Quaterniond()); + // EntityUtils.getScale(this.modelEntity).set(1,1,1); + // //add ambient foliage behavior tree + // AmbientFoliage.attachAmbientFoliageTree(this.modelEntity, 1.0f, foliageType.getGrowthModel().getGrowthRate()); + // } + } else { + this.homogenous = true; + } + this.hasGenerated = true; + } + + /** + * Insert blades of grass into the entity + * @param vX the x offset of the voxel + * @param vY the y offset of the voxel + * @param vZ the z offset of the voxel + * @param floatBufferView the gpu data buffer + * @param chunkData the chunk data + * @return the number of blades of grass added + */ + protected int insertBlades(int vX, int vY, int vZ, FloatBuffer floatBufferView, ChunkData chunkData){ + int rVal = 0; + + //get positions offset + Vector3d voxelRealPos = new Vector3d(this.getRealPos()).add(vX,vY,vZ); + Vector3i currVoxelPos = new Vector3i(this.voxelPos).add(vX,vY,vZ); + + int scale = (int)Math.pow(2,lod); + + //check that the current voxel even supports foliage + boolean shouldGenerate = false; + List foliageTypesSupported = null; + if(chunkData != null && currVoxelPos.y + 1 < ServerTerrainChunk.CHUNK_DIMENSION){ + foliageTypesSupported = Globals.gameConfigCurrent.getVoxelData().getTypeFromId(chunkData.getType(currVoxelPos)).getAmbientFoliage(); + boolean airAbove = chunkData.getType(currVoxelPos.x,currVoxelPos.y+1,currVoxelPos.z) == AIR_VOXEL_ID; + if(foliageTypesSupported != null && airAbove){ + shouldGenerate = true; + } + } + if(shouldGenerate){ + //construct simple grid to place foliage on + Vector3d sample_00 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add(-0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_01 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add(-0.5,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_02 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add(-0.5,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_10 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_11 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_12 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_20 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_21 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0.5,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_22 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).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 + for(int x = 0; x < TARGET_FOLIAGE_SPACING; x=x+scale){ + for(int z = 0; z < TARGET_FOLIAGE_SPACING; z=z+scale){ + //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 - this.getRealPos().y; + double rotVar = placementRandomizer.nextDouble() * Math.PI * 2; + double rotVar2 = placementRandomizer.nextDouble(); + if(floatBufferView.limit() >= floatBufferView.position() + SINGLE_FOLIAGE_DATA_SIZE_BYTES / 4){ + floatBufferView.put((float)offsetX + vX); + floatBufferView.put((float)offsetY + vY); + floatBufferView.put((float)offsetZ + vZ); + floatBufferView.put((float)rotVar); + floatBufferView.put((float)rotVar2); + rVal++; + } + } + } + } + } + } + return rVal; + } + + /** + * Draws all entities in the foliage cell + */ + protected void draw(){ + if(this.modelEntity != null){ + Matrix4d modelMatrix = new Matrix4d(); + Vector3f cameraCenter = CameraEntityUtils.getCameraCenter(Globals.playerCamera); + + RenderPipelineState renderPipelineState = Globals.renderingEngine.getRenderPipelineState(); + OpenGLState openGLState = Globals.renderingEngine.getOpenGLState(); + + Vector3d realPosition = this.getRealPos(); + Vector3f cameraModifiedPosition = new Vector3f((float)realPosition.x,(float)realPosition.y,(float)realPosition.z).sub(cameraCenter); + //frustum check entire cell + int size = (int)Math.pow(2,this.lod); + boolean shouldRender = renderPipelineState.getFrustumIntersection().testSphere( + (float)(cameraModifiedPosition.x + size / 2.0), + (float)(cameraModifiedPosition.y + size / 2.0), + (float)(cameraModifiedPosition.z + size / 2.0), + (float)(size) + ); + if(shouldRender){ + //disable frustum check and instead perform at cell level + boolean currentFrustumCheckState = renderPipelineState.shouldFrustumCheck(); + renderPipelineState.setFrustumCheck(false); + Vector3d grassPosition = EntityUtils.getPosition(modelEntity); + Quaterniond grassRotation = EntityUtils.getRotation(modelEntity); + TextureInstancedActor actor = TextureInstancedActor.getTextureInstancedActor(modelEntity); + + if(actor != null){ + modelMatrix = modelMatrix.identity(); + modelMatrix.translate(cameraModifiedPosition); + modelMatrix.rotate(new Quaterniond(grassRotation)); + modelMatrix.scale(new Vector3d(EntityUtils.getScale(modelEntity))); + actor.applySpatialData(modelMatrix,grassPosition); + + + //draw + actor.draw(renderPipelineState, openGLState); + renderPipelineState.setFrustumCheck(currentFrustumCheckState); + } + } + } + } + + /** + * Gets the real-space position of the foliage cell + * @return the real-space position + */ + protected Vector3d getRealPos(){ + return new Vector3d( + worldPos.x * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET + voxelPos.x, + worldPos.y * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET + voxelPos.y, + worldPos.z * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET + voxelPos.z + ); + } + + /** + * Gets the world-space position of the foliage cell + * @return the world-space position + */ + protected Vector3i getWorldPos(){ + return new Vector3i(worldPos); + } + + /** + * Registers a target foliage cell to notify once this one has completed generating its model + * @param notifyTarget The target to notify + */ + public void registerNotificationTarget(FoliageCell notifyTarget){ + this.notifyTarget = notifyTarget; + } + + /** + * Alerts this foliage cell that a child it is waiting on has generated + */ + public void alertToGeneration(){ + this.generationAlertCount++; + if(this.generationAlertCount >= CHILD_CELLS_PER_PARENT){ + this.destroy(); + } + } + + /** + * Destroys a foliage cell including its physics + */ + public void destroy(){ + if(modelEntity != null){ + Globals.clientScene.registerBehaviorTree(new BehaviorTree(){ + int framesSimulated = 0; + public void simulate(float deltaTime) { + if(framesSimulated < FRAMES_TO_WAIT_BEFORE_DESTRUCTION){ + framesSimulated++; + } else { + ClientEntityUtils.destroyEntity(modelEntity); + Globals.clientScene.deregisterBehaviorTree(this); + } + } + }); + } + } + + /** + * Gets the entity for the cell + * @return The entity if it exists, null otherwise + */ + public Entity getEntity(){ + return modelEntity; + } + + /** + * Transfers chunk data from the source to this foliage cell + * @param source The source foliage cell + */ + public void transferChunkData(FoliageCell source){ + this.chunkData = source.chunkData; + this.homogenous = source.homogenous; + this.hasRequested = source.hasRequested; + } + + /** + * Gets whether this foliage cell has requested its chunk data or not + * @return true if has requested, false otherwise + */ + public boolean hasRequested() { + return hasRequested; + } + + /** + * Sets whether this foliage cell has requested its chunk data or not + * @param hasRequested true if has requested, false otherwise + */ + public void setHasRequested(boolean hasRequested) { + this.hasRequested = hasRequested; + if(!this.hasRequested){ + this.failedGenerationAttempts = 0; + } + } + + /** + * Gets whether this foliage cell has generated its entity or not + * @return true if has generated, false otherwise + */ + public boolean hasGenerated() { + return hasGenerated; + } + + /** + * Sets whether this foliage cell has generated its entity or not + * @param hasGenerated true if has generated, false otherwise + */ + public void setHasGenerated(boolean hasGenerated) { + this.hasGenerated = hasGenerated; + } + + /** + * Sets whether this foliage cell is homogenous or not + * @param hasGenerated true if is homogenous, false otherwise + */ + public void setHomogenous(boolean homogenous) { + this.homogenous = homogenous; + } + + /** + * Gets the number of failed generation attempts + * @return The number of failed generation attempts + */ + public int getFailedGenerationAttempts(){ + return failedGenerationAttempts; + } + + /** + * Sets the number of failed generation attempts + * @param attempts The number of failed generation attempts + */ + public void setFailedGenerationAttempts(int attempts){ + this.failedGenerationAttempts = this.failedGenerationAttempts + attempts; + } + + /** + * Gets whether this foliage cell is homogenous or not + * @return true if it is homogenous, false otherwise + */ + public boolean isHomogenous(){ + return homogenous; + } + + /** + * Gets the minimum distance from a node to a point + * @param pos the position to check against + * @param node the node + * @param distCache the lod value under which distance caches are invalidated + * @return the distance + */ + public double getMinDistance(Vector3i worldPos, WorldOctTreeNode node, int distCache){ + if(cachedMinDistance != INVALID_DIST_CACHE && distCache < lod){ + return cachedMinDistance; + } else { + this.cachedMinDistance = GeomUtils.getMinSquaredDistanceAABB(worldPos, node.getMinBound(), node.getMaxBound()); + return cachedMinDistance; + } + } + + +} diff --git a/src/main/java/electrosphere/client/terrain/foliage/FoliageCellManager.java b/src/main/java/electrosphere/client/terrain/foliage/FoliageCellManager.java new file mode 100644 index 00000000..2122a601 --- /dev/null +++ b/src/main/java/electrosphere/client/terrain/foliage/FoliageCellManager.java @@ -0,0 +1,876 @@ +package electrosphere.client.terrain.foliage; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.joml.Vector3d; +import org.joml.Vector3i; + +import electrosphere.engine.Globals; +import electrosphere.entity.EntityUtils; +import electrosphere.game.data.foliage.type.FoliageType; +import electrosphere.logger.LoggerInterface; +import electrosphere.server.terrain.manager.ServerTerrainChunk; +import electrosphere.util.ds.octree.WorldOctTree; +import electrosphere.util.ds.octree.WorldOctTree.WorldOctTreeNode; +import electrosphere.util.math.GeomUtils; + +/** + * Manages foliage cells on the client + */ +public class FoliageCellManager { + + /** + * Number of times to try updating per frame. Lower this to reduce lag but slow down terrain mesh generation. + */ + static final int UPDATE_ATTEMPTS_PER_FRAME = 3; + + /** + * The number of generation attempts before a cell is marked as having not requested its data + */ + static final int FAILED_GENERATION_ATTEMPT_THRESHOLD = 250; + + /** + * The distance to foliage at full resolution + */ + public static final double FULL_RES_DIST = 16 * 16; + + /** + * The distance for half resolution + */ + public static final double HALF_RES_DIST = 20 * 20; + + /** + * The distance for quarter resolution + */ + public static final double QUARTER_RES_DIST = 24 * 24; + + /** + * The distance for eighth resolution + */ + public static final double EIGHTH_RES_DIST = 32 * 32; + + /** + * The distance for sixteenth resolution + */ + public static final double SIXTEENTH_RES_DIST = 48 * 48; + + /** + * Lod value for a full res chunk + */ + public static final int FULL_RES_LOD = 0; + + /** + * Lod value for a half res chunk + */ + public static final int HALF_RES_LOD = 1; + + /** + * Lod value for a quarter res chunk + */ + public static final int QUARTER_RES_LOD = 2; + + /** + * Lod value for a eighth res chunk + */ + public static final int EIGHTH_RES_LOD = 3; + + /** + * Lod value for a sixteenth res chunk + */ + public static final int SIXTEENTH_RES_LOD = 4; + + /** + * Lod value for evaluating all lod levels + */ + public static final int ALL_RES_LOD = 5; + + /** + * The octree holding all the chunks to evaluate + */ + WorldOctTree chunkTree; + + /** + * Tracks what nodes have been evaluated this frame -- used to deduplicate evaluation calls + */ + Map,Boolean> evaluationMap = new HashMap,Boolean>(); + + /** + * The last recorded player world position + */ + Vector3i lastPlayerPos = new Vector3i(); + + /** + * Tracks whether the cell manager updated last frame or not + */ + boolean updatedLastFrame = true; + + /** + * Controls whether the foliage cell manager should update or not + */ + boolean shouldUpdate = true; + + /** + * The dimensions of the world + */ + int worldDim = 0; + + /** + * Tracks the number of currently valid cells (ie didn't require an update this frame) + */ + int validCellCount = 0; + + /** + * The number of maximum resolution chunks + */ + int maxResCount = 0; + + /** + * The number of half resolution chunks + */ + int halfResCount = 0; + + /** + * The number of generated chunks + */ + int generated = 0; + + /** + * Tracks whether the cell manager has initialized or not + */ + boolean initialized = false; + + /** + * Constructor + * @param worldDim The size of the world in chunks + */ + public FoliageCellManager(int worldDim){ + this.chunkTree = new WorldOctTree( + new Vector3i(0,0,0), + new Vector3i(worldDim * ServerTerrainChunk.CHUNK_DIMENSION, worldDim * ServerTerrainChunk.CHUNK_DIMENSION, worldDim * ServerTerrainChunk.CHUNK_DIMENSION) + ); + this.chunkTree.getRoot().setData(FoliageCell.generateTerrainCell(new Vector3i(0,0,0), chunkTree.getMaxLevel())); + this.worldDim = worldDim; + } + + /** + * Inits the foliage cell data + */ + public void init(){ + //queue ambient foliage models + for(FoliageType foliageType : Globals.gameConfigCurrent.getFoliageMap().getFoliageList()){ + if(foliageType.getTokens().contains(FoliageType.TOKEN_AMBIENT)){ + Globals.assetManager.addModelPathToQueue(foliageType.getGraphicsTemplate().getModel().getPath()); + Globals.assetManager.addShaderToQueue(FoliageCell.vertexPath, FoliageCell.fragmentPath); + } + } + } + + /** + * Updates all cells in the chunk + */ + public void update(){ + Globals.profiler.beginCpuSample("FoliageCellManager.update"); + if(shouldUpdate && Globals.playerEntity != null){ + Vector3d playerPos = EntityUtils.getPosition(Globals.playerEntity); + Vector3i absVoxelPos = Globals.clientWorldData.convertRealToAbsoluteVoxelSpace(playerPos); + int distCache = this.getDistCache(this.lastPlayerPos, absVoxelPos); + this.lastPlayerPos.set(absVoxelPos); + //the sets to iterate through + updatedLastFrame = true; + validCellCount = 0; + evaluationMap.clear(); + //update all full res cells + WorldOctTreeNode rootNode = this.chunkTree.getRoot(); + Globals.profiler.beginCpuSample("FoliageCellManager.update - full res cells"); + updatedLastFrame = this.recursivelyUpdateCells(rootNode, absVoxelPos, evaluationMap, SIXTEENTH_RES_LOD, distCache); + Globals.profiler.endCpuSample(); + if(!updatedLastFrame && !this.initialized){ + this.initialized = true; + } + } + Globals.profiler.endCpuSample(); + } + + /** + * Recursively update child nodes + * @param node The root node + * @param absVoxelPos The player's position + * @param minLeafLod The minimum LOD required to evaluate a leaf + * @param evaluationMap Map of leaf nodes that have been evaluated this frame + * @return true if there is work remaining to be done, false otherwise + */ + private boolean recursivelyUpdateCells(WorldOctTreeNode node, Vector3i absVoxelPos, Map,Boolean> evaluationMap, int minLeafLod, int distCache){ + boolean updated = false; + if(evaluationMap.containsKey(node)){ + return false; + } + if(node.getData().hasGenerated() && node.getData().isHomogenous()){ + return false; + } + if(node.isLeaf()){ + if(this.shouldSplit(absVoxelPos, node, distCache)){ + Globals.profiler.beginCpuSample("FoliageCellManager.split"); + //perform op + WorldOctTreeNode container = chunkTree.split(node); + FoliageCell containerCell = FoliageCell.generateTerrainCell(container.getMinBound(), this.chunkTree.getMaxLevel() - container.getLevel()); + container.setData(containerCell); + container.getData().transferChunkData(node.getData()); + + //do creations + container.getChildren().forEach(child -> { + Vector3i cellWorldPos = new Vector3i( + child.getMinBound().x, + child.getMinBound().y, + child.getMinBound().z + ); + FoliageCell foliageCell = FoliageCell.generateTerrainCell(cellWorldPos,this.chunkTree.getMaxLevel() - child.getLevel()); + foliageCell.registerNotificationTarget(node.getData()); + child.setLeaf(true); + child.setData(foliageCell); + evaluationMap.put(child,true); + }); + + //do deletions + this.twoLayerDestroy(node); + + //update neighbors + this.conditionalUpdateAdjacentNodes(container, container.getChildren().get(0).getLevel()); + + Globals.profiler.endCpuSample(); + updated = true; + } else if(this.shouldRequest(absVoxelPos, node, minLeafLod, distCache)){ + Globals.profiler.beginCpuSample("FoliageCellManager.request"); + + //calculate what to request + FoliageCell cell = node.getData(); + + //actually send requests + if(this.requestChunks(node)){ + cell.setHasRequested(true); + } + evaluationMap.put(node,true); + + Globals.profiler.endCpuSample(); + updated = true; + } else if(this.shouldGenerate(absVoxelPos, node, minLeafLod, distCache)){ + Globals.profiler.beginCpuSample("FoliageCellManager.generate"); + int lodLevel = this.getLODLevel(node); + + if(this.containsDataToGenerate(node)){ + node.getData().generateDrawableEntity(lodLevel); + if(node.getData().getFailedGenerationAttempts() > FAILED_GENERATION_ATTEMPT_THRESHOLD){ + node.getData().setHasRequested(false); + } + } else if(node.getData() != null){ + node.getData().setFailedGenerationAttempts(node.getData().getFailedGenerationAttempts() + 1); + if(node.getData().getFailedGenerationAttempts() > FAILED_GENERATION_ATTEMPT_THRESHOLD){ + node.getData().setHasRequested(false); + } + } + evaluationMap.put(node,true); + Globals.profiler.endCpuSample(); + updated = true; + } + } else { + if(this.shouldJoin(absVoxelPos, node, distCache)) { + this.join(node); + updated = true; + } else { + this.validCellCount++; + List> children = node.getChildren(); + boolean isHomogenous = true; + boolean fullyGenerated = true; + for(int i = 0; i < 8; i++){ + WorldOctTreeNode child = children.get(i); + boolean childUpdate = this.recursivelyUpdateCells(child, absVoxelPos, evaluationMap, minLeafLod, distCache); + if(childUpdate == true){ + updated = true; + } + if(!child.getData().hasGenerated()){ + fullyGenerated = false; + } + if(!child.getData().isHomogenous()){ + isHomogenous = false; + } + } + WorldOctTreeNode newNode = null; + if(isHomogenous){ + newNode = this.join(node); + newNode.getData().setHomogenous(true); + } + if(fullyGenerated && newNode != null){ + newNode.getData().setHasGenerated(true); + } + if((this.chunkTree.getMaxLevel() - node.getLevel()) < minLeafLod){ + evaluationMap.put(node,true); + } + } + } + return updated; + } + + /** + * Draw all foliage cells + */ + public void draw(){ + this.recursivelyDraw(this.chunkTree.getRoot()); + } + + /** + * Draws all foliage cells recursively + * @param node The current node + */ + private void recursivelyDraw(WorldOctTreeNode node){ + if(node.getChildren() != null && node.getChildren().size() > 0){ + for(int i = 0; i < 8; i++){ + this.recursivelyDraw(node.getChildren().get(i)); + } + } + node.getData().draw(); + } + + /** + * 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(Vector3i worldPos, WorldOctTreeNode node, int distCache){ + if(node.getData() == null){ + return GeomUtils.getMinSquaredDistanceAABB(worldPos, node.getMinBound(), node.getMaxBound()); + } else { + return node.getData().getMinDistance(worldPos, node, distCache); + } + } + + /** + * Gets the distance cache value + * @param lastPlayerPos The last player world position + * @param currentPlayerPos The current player world position + * @return The distance cache value + */ + private int getDistCache(Vector3i lastPlayerPos, Vector3i currentPlayerPos){ + if( + lastPlayerPos.x / 16 != currentPlayerPos.x / 16 || lastPlayerPos.z / 16 != currentPlayerPos.z / 16 || lastPlayerPos.z / 16 != currentPlayerPos.z / 16 + ){ + return this.chunkTree.getMaxLevel(); + } + if( + lastPlayerPos.x / 16 != currentPlayerPos.x / 16 || lastPlayerPos.z / 16 != currentPlayerPos.z / 16 || lastPlayerPos.z / 16 != currentPlayerPos.z / 16 + ){ + return SIXTEENTH_RES_LOD + 2; + } + if( + lastPlayerPos.x / 8 != currentPlayerPos.x / 8 || lastPlayerPos.z / 8 != currentPlayerPos.z / 8 || lastPlayerPos.z / 8 != currentPlayerPos.z / 8 + ){ + return SIXTEENTH_RES_LOD + 1; + } + if( + lastPlayerPos.x / 4 != currentPlayerPos.x / 4 || lastPlayerPos.z / 4 != currentPlayerPos.z / 4 || lastPlayerPos.z / 4 != currentPlayerPos.z / 4 + ){ + return SIXTEENTH_RES_LOD; + } + if( + lastPlayerPos.x / 2 != currentPlayerPos.x / 2 || lastPlayerPos.z / 2 != currentPlayerPos.z / 2 || lastPlayerPos.z / 2 != currentPlayerPos.z / 2 + ){ + return EIGHTH_RES_LOD; + } + if( + lastPlayerPos.x != currentPlayerPos.x || lastPlayerPos.z != currentPlayerPos.z || lastPlayerPos.z != currentPlayerPos.z + ){ + return QUARTER_RES_LOD; + } + return -1; + } + + /** + * 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(Vector3i pos, WorldOctTreeNode node, int distCache){ + //breaking out into dedicated function so can add case handling ie if we want + //to combine fullres nodes into larger nodes to conserve on foliage calls + return + node.canSplit() && + (node.getLevel() != this.chunkTree.getMaxLevel()) && + !node.getData().isHomogenous() && + (node.getParent() != null || node == this.chunkTree.getRoot()) && + ( + ( + node.getLevel() < this.chunkTree.getMaxLevel() - SIXTEENTH_RES_LOD && + this.getMinDistance(pos, node, distCache) <= SIXTEENTH_RES_DIST + ) + || + ( + node.getLevel() < this.chunkTree.getMaxLevel() - EIGHTH_RES_LOD && + this.getMinDistance(pos, node, distCache) <= EIGHTH_RES_DIST + ) + || + ( + node.getLevel() < this.chunkTree.getMaxLevel() - QUARTER_RES_LOD && + this.getMinDistance(pos, node, distCache) <= QUARTER_RES_DIST + ) + // || + // ( + // node.getLevel() < this.chunkTree.getMaxLevel() - HALF_RES_LOD && + // this.getMinDistance(pos, node, distCache) <= HALF_RES_DIST + // ) + // || + // ( + // node.getLevel() < this.chunkTree.getMaxLevel() && + // this.getMinDistance(pos, node, distCache) <= FULL_RES_DIST + // ) + ) + ; + } + + /** + * Gets the LOD level of the foliage cell + * @param node The node to consider + * @return -1 if outside of render range, -1 if the node is not a valid foliage cell leaf, otherwise returns the LOD level + */ + private int getLODLevel(WorldOctTreeNode node){ + return this.chunkTree.getMaxLevel() - node.getLevel(); + } + + /** + * Conditionally updates all adjacent nodes if their level would require transition cells in the voxel rasterization + * @param node The node to search from adjacencies from + * @param level The level to check against + */ + private void conditionalUpdateAdjacentNodes(WorldOctTreeNode node, int level){ + //don't bother to check if it's a lowest-res chunk + if(this.chunkTree.getMaxLevel() - level > FoliageCellManager.FULL_RES_LOD){ + return; + } + if(node.getMinBound().x - 1 >= 0){ + WorldOctTreeNode xNegNode = this.chunkTree.search(new Vector3i(node.getMinBound()).add(-1,0,0), false); + if(xNegNode != null && xNegNode.getLevel() < level){ + xNegNode.getData().setHasGenerated(false); + } + } + if(node.getMinBound().y - 1 >= 0){ + WorldOctTreeNode yNegNode = this.chunkTree.search(new Vector3i(node.getMinBound()).add(0,-1,0), false); + if(yNegNode != null && yNegNode.getLevel() < level){ + yNegNode.getData().setHasGenerated(false); + } + } + if(node.getMinBound().z - 1 >= 0){ + WorldOctTreeNode zNegNode = this.chunkTree.search(new Vector3i(node.getMinBound()).add(0,0,-1), false); + if(zNegNode != null && zNegNode.getLevel() < level){ + zNegNode.getData().setHasGenerated(false); + } + } + if(node.getMaxBound().x + 1 < this.worldDim){ + WorldOctTreeNode xPosNode = this.chunkTree.search(new Vector3i(node.getMaxBound()).add(1,-1,-1), false); + if(xPosNode != null && xPosNode.getLevel() < level){ + xPosNode.getData().setHasGenerated(false); + } + } + if(node.getMaxBound().y + 1 < this.worldDim){ + WorldOctTreeNode yPosNode = this.chunkTree.search(new Vector3i(node.getMaxBound()).add(-1,1,-1), false); + if(yPosNode != null && yPosNode.getLevel() < level){ + yPosNode.getData().setHasGenerated(false); + } + } + if(node.getMaxBound().z + 1 < this.worldDim){ + WorldOctTreeNode zPosNode = this.chunkTree.search(new Vector3i(node.getMaxBound()).add(-1,-1,1), false); + if(zPosNode != null && zPosNode.getLevel() < level){ + zPosNode.getData().setHasGenerated(false); + } + } + } + + /** + * 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(Vector3i pos, WorldOctTreeNode node, int distCache){ + //breaking out into dedicated function so can add case handling ie if we want + //to combine fullres nodes into larger nodes to conserve on foliage calls + return + node.getLevel() > 0 && + (node.getLevel() != this.chunkTree.getMaxLevel()) && + ( + ( + node.getLevel() == this.chunkTree.getMaxLevel() - HALF_RES_LOD && + this.getMinDistance(pos, node, distCache) > FULL_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - QUARTER_RES_LOD && + this.getMinDistance(pos, node, distCache) > HALF_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - EIGHTH_RES_LOD && + this.getMinDistance(pos, node, distCache) > QUARTER_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - SIXTEENTH_RES_LOD && + this.getMinDistance(pos, node, distCache) > EIGHTH_RES_DIST + ) + || + ( + this.getMinDistance(pos, node, distCache) > SIXTEENTH_RES_DIST + ) + ) + ; + } + + /** + * Joins a parent node + * @param node The parent node + */ + private WorldOctTreeNode join(WorldOctTreeNode node){ + Globals.profiler.beginCpuSample("FoliageCellManager.join"); + + //queue destructions prior to join -- the join operator clears all children on node + this.recursivelyDestroy(node); + + //perform op + FoliageCell newLeafCell = FoliageCell.generateTerrainCell(node.getMinBound(),node.getData().lod); + WorldOctTreeNode newLeaf = chunkTree.join(node, newLeafCell); + newLeaf.getData().transferChunkData(node.getData()); + + //update neighbors + this.conditionalUpdateAdjacentNodes(newLeaf, newLeaf.getLevel()); + evaluationMap.put(newLeaf,true); + + Globals.profiler.endCpuSample(); + return newLeaf; + } + + /** + * Checks if this cell should request chunk data + * @param pos the player's position + * @param node the node + * @param minLeafLod The minimum LOD required to evaluate a leaf + * @return true if should request chunk data, false otherwise + */ + public boolean shouldRequest(Vector3i pos, WorldOctTreeNode node, int minLeafLod, int distCache){ + return + node.getData() != null && + !node.getData().hasRequested() && + (this.chunkTree.getMaxLevel() - node.getLevel()) <= minLeafLod && + ( + ( + node.getLevel() == this.chunkTree.getMaxLevel() + // && + // this.getMinDistance(pos, node) <= FULL_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - HALF_RES_LOD + && + this.getMinDistance(pos, node, distCache) <= QUARTER_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - QUARTER_RES_LOD + && + this.getMinDistance(pos, node, distCache) <= EIGHTH_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - EIGHTH_RES_LOD + && + this.getMinDistance(pos, node, distCache) <= SIXTEENTH_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - SIXTEENTH_RES_LOD + && + this.getMinDistance(pos, node, distCache) <= SIXTEENTH_RES_DIST + ) + ) + ; + } + + /** + * Checks if this cell should generate + * @param pos the player's position + * @param node the node + * @param minLeafLod The minimum LOD required to evaluate a leaf + * @return true if should generate, false otherwise + */ + public boolean shouldGenerate(Vector3i pos, WorldOctTreeNode node, int minLeafLod, int distCache){ + return + !node.getData().hasGenerated() && + (this.chunkTree.getMaxLevel() - node.getLevel()) <= minLeafLod && + ( + ( + node.getLevel() == this.chunkTree.getMaxLevel() + // && + // this.getMinDistance(pos, node) <= FULL_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - HALF_RES_LOD + && + this.getMinDistance(pos, node, distCache) <= QUARTER_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - QUARTER_RES_LOD + && + this.getMinDistance(pos, node, distCache) <= EIGHTH_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - EIGHTH_RES_LOD + && + this.getMinDistance(pos, node, distCache) <= SIXTEENTH_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - SIXTEENTH_RES_LOD + && + this.getMinDistance(pos, node, distCache) <= SIXTEENTH_RES_DIST + ) + ) + ; + } + + /** + * Checks if the node should have destroy called on it + * @param node The node + * @return true if should destroy, false otherwise + */ + public boolean shouldDestroy(WorldOctTreeNode node){ + return + node.getData() != null && + node.getData().getEntity() != null + ; + } + + /** + * Destroys the foliage chunk + */ + protected void destroy(){ + this.recursivelyDestroy(this.chunkTree.getRoot()); + } + + /** + * Recursively destroy a tree + * @param node The root of the tree + */ + private void recursivelyDestroy(WorldOctTreeNode node){ + if(node.getChildren().size() > 0){ + for(WorldOctTreeNode child : node.getChildren()){ + child.getData().destroy(); + } + // node.getChildren().forEach(child -> recursivelyDestroy(child)); + } + if(node.getData() != null){ + node.getData().destroy(); + } + } + + /** + * Destroys two layers of nodes + * @param node The top node + */ + private void twoLayerDestroy(WorldOctTreeNode node){ + if(!node.getData().hasGenerated()){ + for(WorldOctTreeNode child : node.getChildren()){ + child.getData().destroy(); + } + } else { + node.getData().destroy(); + } + } + + /** + * Checks if the cell manager made an update last frame or not + * @return true if an update occurred, false otherwise + */ + public boolean updatedLastFrame(){ + return this.updatedLastFrame; + } + + /** + * Checks if the position is within the full LOD range + * @param worldPos The world position + * @return true if within full LOD range, false otherwise + */ + public boolean isFullLOD(Vector3i worldPos){ + Vector3d playerRealPos = EntityUtils.getPosition(Globals.playerEntity); + Vector3d chunkMin = Globals.clientWorldData.convertWorldToRealSpace(worldPos); + Vector3d chunkMax = Globals.clientWorldData.convertWorldToRealSpace(new Vector3i(worldPos).add(1,1,1)); + return GeomUtils.getMinDistanceAABB(playerRealPos, chunkMin, chunkMax) <= FULL_RES_DIST; + } + + /** + * Evicts all cells + */ + public void evictAll(){ + this.recursivelyDestroy(this.chunkTree.getRoot()); + this.chunkTree.clear(); + } + + + /** + * Marks a foliage cell as updateable + * @param worldX The world x position + * @param worldY The world y position + * @param worldZ The world z position + */ + public void markUpdateable(float worldX, float worldY, float worldZ){ + throw new Error("Unimplemented"); + } + + /** + * Requests all chunks for a given foliage cell + * @param cell The cell + * @return true if all cells were successfully requested, false otherwise + */ + private boolean requestChunks(WorldOctTree.WorldOctTreeNode node){ + Vector3i worldPos = node.getMinBound(); + if( + worldPos.x >= 0 && + worldPos.x < Globals.clientWorldData.getWorldDiscreteSize() && + worldPos.y >= 0 && + worldPos.y < Globals.clientWorldData.getWorldDiscreteSize() && + worldPos.z >= 0 && + worldPos.z < Globals.clientWorldData.getWorldDiscreteSize() && + !Globals.clientTerrainManager.containsChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, 0) + ){ + //client should request chunk data from server for each chunk necessary to create the model + LoggerInterface.loggerNetworking.DEBUG("(Client) Send Request for terrain at " + worldPos); + if(!Globals.clientTerrainManager.requestChunk(worldPos.x, worldPos.y, worldPos.z, 0)){ + return false; + } + } + return true; + } + + /** + * Checks if all chunk data required to generate this foliage cell is present + * @param node The node + * @param highResFace The higher resolution face of a not-full-resolution chunk. Null if the chunk is max resolution or there is no higher resolution face for the current chunk + * @return true if all data is available, false otherwise + */ + private boolean containsDataToGenerate(WorldOctTree.WorldOctTreeNode node){ + FoliageCell cell = node.getData(); + Vector3i worldPos = cell.getWorldPos(); + if(!Globals.clientTerrainManager.containsChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, 0)){ + return false; + } + return true; + } + + /** + * Sets whether the foliage cell manager should update or not + * @param shouldUpdate true if should update, false otherwise + */ + public void setShouldUpdate(boolean shouldUpdate){ + this.shouldUpdate = shouldUpdate; + } + + /** + * Gets whether the client foliage cell manager should update or not + * @return true if should update, false otherwise + */ + public boolean getShouldUpdate(){ + return this.shouldUpdate; + } + + /** + * Gets the number of currently valid cells + * @return The number of currently valid cells + */ + public int getValidCellCount(){ + return validCellCount; + } + + /** + * Calculates the status of the foliage cell manager + */ + public void updateStatus(){ + maxResCount = 0; + halfResCount = 0; + generated = 0; + this.recursivelyCalculateStatus(this.chunkTree.getRoot()); + } + + /** + * Recursively calculates the status of the manager + * @param node The root node + */ + private void recursivelyCalculateStatus(WorldOctTreeNode node){ + if(node.getLevel() == this.chunkTree.getMaxLevel() - 1){ + halfResCount++; + } + if(node.getLevel() == this.chunkTree.getMaxLevel()){ + maxResCount++; + } + if(node.getData() != null && node.getData().hasGenerated()){ + generated++; + } + if(node.getChildren() != null && node.getChildren().size() > 0){ + List> children = new LinkedList>(node.getChildren()); + for(WorldOctTreeNode child : children){ + recursivelyCalculateStatus(child); + } + } + } + + /** + * Gets The number of maximum resolution chunks + * @return The number of maximum resolution chunks + */ + public int getMaxResCount() { + return maxResCount; + } + + /** + * Gets The number of half resolution chunks + * @return The number of half resolution chunks + */ + public int getHalfResCount() { + return halfResCount; + } + + /** + * Gets The number of generated chunks + * @return + */ + public int getGenerated() { + return generated; + } + + /** + * Gets whether the client foliage cell manager has initialized or not + * @return true if it has initialized, false otherwise + */ + public boolean isInitialized(){ + return this.initialized; + } + + /** + * Gets the foliage cell for a given world coordinate if it has been generated + * @param worldX The world x coordinate + * @param worldY The world y coordinate + * @param worldZ The world z coordinate + * @return The foliage cell if it exists, null otherwise + */ + public FoliageCell getFoliageCell(int worldX, int worldY, int worldZ){ + WorldOctTreeNode node = this.chunkTree.search(new Vector3i(worldX,worldY,worldZ), false); + if(node != null){ + return node.getData(); + } + return null; + } + + + +} diff --git a/src/main/java/electrosphere/client/terrain/foliage/FoliageModel.java b/src/main/java/electrosphere/client/terrain/foliage/FoliageModel.java new file mode 100644 index 00000000..ae92c21e --- /dev/null +++ b/src/main/java/electrosphere/client/terrain/foliage/FoliageModel.java @@ -0,0 +1,338 @@ +package electrosphere.client.terrain.foliage; + +import java.nio.ByteBuffer; +import java.nio.FloatBuffer; +import java.util.List; +import java.util.Random; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.joml.Quaterniond; +import org.joml.Vector3d; +import org.joml.Vector3i; +import org.lwjgl.BufferUtils; + +import electrosphere.client.terrain.cache.ChunkData; +import electrosphere.engine.Globals; +import electrosphere.engine.assetmanager.queue.QueuedTexture; +import electrosphere.entity.ClientEntityUtils; +import electrosphere.entity.Entity; +import electrosphere.entity.EntityCreationUtils; +import electrosphere.entity.EntityUtils; +import electrosphere.entity.state.foliage.AmbientFoliage; +import electrosphere.game.data.foliage.type.FoliageType; +import electrosphere.logger.LoggerInterface; +import electrosphere.renderer.actor.instance.TextureInstancedActor; +import electrosphere.server.terrain.manager.ServerTerrainChunk; + +/** + * Generates a foliage model + */ +public class FoliageModel { + + /** + * Number of frames to wait before destroying the chunk entity + */ + public static final int FRAMES_TO_WAIT_BEFORE_DESTRUCTION = 25; + + /** + * Number of child cells per parent cell + */ + static final int CHILD_CELLS_PER_PARENT = 8; + + /** + * Wiggle room in number of entries + */ + static final int BUFFER_WIGGLE_ROOM = 200; + + /** + * 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 + BUFFER_WIGGLE_ROOM; + + /** + * The length of the ray to ground test with + */ + static final float RAY_LENGTH = 1.0f; + + /** + * The height above the chunk to start from when sampling downwards + */ + static final float SAMPLE_START_HEIGHT = 0.5f; + + /** + * The ID of the air voxel + */ + static final int AIR_VOXEL_ID = 0; + + /** + *

+ * 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; + + /** + * Vertex shader path + */ + protected static final String vertexPath = "Shaders/entities/foliage/foliage.vs"; + + /** + * Fragment shader path + */ + protected static final String fragmentPath = "Shaders/entities/foliage/foliage.fs"; + + /** + * Used for generating foliage cells + */ + static final ExecutorService generationService = Executors.newFixedThreadPool(2); + + /** + * Creates a client foliage chunk based on weights and values provided + * @param toDelete The entity to delete on full generation of this entity + * @param notifyTarget The target draw cell to notify once this has successfully generated its model + * @param levelOfDetail Increasing value that increments level of detail. 0 would be full resolution, 1 would be half resolution and so on. Only generates physics if levelOfDetail is 0 + * @param hasFoliage true if the chunk has polygons to generate a model with, false otherwise + * @return The terrain chunk entity + */ + public static Entity clientCreateFoliageChunkEntity( + List foliageTypesSupported, + int scale, + Entity modelEntity, + Vector3d realPos, + Vector3i worldPos, + Vector3i voxelPos, + FoliageCell notifyTarget, + Entity toDelete + ){ + Globals.profiler.beginAggregateCpuSample("FoliageModel.clientCreateFoliageChunkEntity"); + + Entity rVal = EntityCreationUtils.createClientSpatialEntity(); + + generationService.submit(() -> { + try { + Random placementRandomizer = new Random(); + //get type + String foliageTypeName = foliageTypesSupported.get(0); + FoliageType foliageType = Globals.gameConfigCurrent.getFoliageMap().getFoliage(foliageTypeName); + + //create cell and buffer + ByteBuffer buffer = BufferUtils.createByteBuffer(TARGET_FOLIAGE_PER_CELL * SINGLE_FOLIAGE_DATA_SIZE_BYTES); + if(buffer.capacity() < TARGET_FOLIAGE_PER_CELL * SINGLE_FOLIAGE_DATA_SIZE_BYTES){ + LoggerInterface.loggerEngine.WARNING("Failed to allocate data for foliage cell! " + buffer.limit()); + } + FloatBuffer floatBufferView = buffer.asFloatBuffer(); + int drawCount = 0; + for(int x = 0; x < scale; x++){ + for(int y = 0; y < scale; y++){ + for(int z = 0; z < scale; z++){ + ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos,ChunkData.NO_STRIDE); + if(data == null){ + continue; + } + drawCount = drawCount + FoliageModel.insertBlades( + realPos, voxelPos, + scale, placementRandomizer, + x, y, z, + floatBufferView, data + ); + } + } + } + if(drawCount > 0){ + buffer.position(0); + buffer.limit(TARGET_FOLIAGE_PER_CELL * SINGLE_FOLIAGE_DATA_SIZE_BYTES); + //construct data texture + QueuedTexture queuedAsset = new QueuedTexture(buffer,SINGLE_FOLIAGE_DATA_SIZE_BYTES / 4,TARGET_FOLIAGE_PER_CELL); + Globals.assetManager.queuedAsset(queuedAsset); + + TextureInstancedActor.attachTextureInstancedActor(modelEntity, foliageType.getGraphicsTemplate().getModel().getPath(), vertexPath, fragmentPath, queuedAsset, drawCount); + ClientEntityUtils.initiallyPositionEntity(modelEntity, realPos, new Quaterniond()); + EntityUtils.getScale(modelEntity).set(1,1,1); + //add ambient foliage behavior tree + AmbientFoliage.attachAmbientFoliageTree(modelEntity, 1.0f, foliageType.getGrowthModel().getGrowthRate()); + } + } catch (Error e){ + LoggerInterface.loggerEngine.ERROR(e); + } catch(Exception e){ + LoggerInterface.loggerEngine.ERROR(e); + } + }); + + Globals.profiler.endCpuSample(); + return rVal; + } + + /** + * Insert blades of grass into the entity + * @param vX the x offset of the voxel + * @param vY the y offset of the voxel + * @param vZ the z offset of the voxel + * @param floatBufferView the gpu data buffer + * @param chunkData the chunk data + * @return the number of blades of grass added + */ + protected static int insertBlades( + Vector3d realPos, Vector3i voxelPos, + int scale, Random placementRandomizer, + int vX, int vY, int vZ, + FloatBuffer floatBufferView, ChunkData chunkData + ){ + int rVal = 0; + + //get positions offset + Vector3d voxelRealPos = new Vector3d(realPos).add(vX,vY,vZ); + Vector3i currVoxelPos = new Vector3i(voxelPos).add(vX,vY,vZ); + + //check that the current voxel even supports foliage + boolean shouldGenerate = false; + List foliageTypesSupported = null; + if(chunkData != null && currVoxelPos.y + 1 < ServerTerrainChunk.CHUNK_DIMENSION){ + foliageTypesSupported = Globals.gameConfigCurrent.getVoxelData().getTypeFromId(chunkData.getType(currVoxelPos)).getAmbientFoliage(); + boolean airAbove = chunkData.getType(currVoxelPos.x,currVoxelPos.y+1,currVoxelPos.z) == AIR_VOXEL_ID; + if(foliageTypesSupported != null && airAbove){ + shouldGenerate = true; + } + } + if(shouldGenerate){ + //construct simple grid to place foliage on + Vector3d sample_00 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add(-0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_01 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add(-0.5,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_02 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add(-0.5,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_10 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_11 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_12 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_20 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_21 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0.5,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH); + Vector3d sample_22 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).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 + for(int x = 0; x < TARGET_FOLIAGE_SPACING; x=x+scale){ + for(int z = 0; z < TARGET_FOLIAGE_SPACING; z=z+scale){ + //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(); + if(floatBufferView.limit() >= floatBufferView.position() + SINGLE_FOLIAGE_DATA_SIZE_BYTES / 4){ + floatBufferView.put((float)offsetX + vX); + floatBufferView.put((float)offsetY + vY); + floatBufferView.put((float)offsetZ + vZ); + floatBufferView.put((float)rotVar); + floatBufferView.put((float)rotVar2); + rVal++; + } + } + } + } + } + } + return rVal; + } + + /** + * Shuts down the model generation threads + */ + public static void haltThreads(){ + generationService.shutdown(); + } + +} diff --git a/src/main/java/electrosphere/collision/PhysicsEntityUtils.java b/src/main/java/electrosphere/collision/PhysicsEntityUtils.java index b14ba7ed..99c7c2f6 100644 --- a/src/main/java/electrosphere/collision/PhysicsEntityUtils.java +++ b/src/main/java/electrosphere/collision/PhysicsEntityUtils.java @@ -523,7 +523,8 @@ public class PhysicsEntityUtils { collisionEngine.getCollidables(); throw new Error("Collision engine collidables are null!"); } - for(Collidable collidable : collidableList){ + for(int i = 0; i < collidableList.size(); i++){ + Collidable collidable = collidableList.get(i); Entity entity = collidable.getParent(); DBody body = PhysicsEntityUtils.getDBody(entity); if(body != null && body.isEnabled() && !body.isKinematic()){ diff --git a/src/main/java/electrosphere/engine/Globals.java b/src/main/java/electrosphere/engine/Globals.java index 10509186..ed6747c9 100644 --- a/src/main/java/electrosphere/engine/Globals.java +++ b/src/main/java/electrosphere/engine/Globals.java @@ -17,13 +17,13 @@ import electrosphere.client.chemistry.ClientChemistryCollisionCallback; import electrosphere.client.entity.particle.ParticleService; import electrosphere.client.fluid.cells.FluidCellManager; import electrosphere.client.fluid.manager.ClientFluidManager; -import electrosphere.client.foliagemanager.ClientFoliageManager; import electrosphere.client.player.ClientPlayerData; import electrosphere.client.scene.ClientSceneWrapper; import electrosphere.client.scene.ClientWorldData; import electrosphere.client.sim.ClientSimulation; import electrosphere.client.terrain.cells.ClientDrawCellManager; import electrosphere.client.terrain.cells.VoxelTextureAtlas; +import electrosphere.client.terrain.foliage.FoliageCellManager; import electrosphere.client.terrain.manager.ClientTerrainManager; import electrosphere.client.ui.menu.WindowUtils; import electrosphere.collision.CollisionEngine; @@ -343,7 +343,8 @@ public class Globals { public static InstanceManager clientInstanceManager = new InstanceManager(); //client side foliage manager - public static ClientFoliageManager clientFoliageManager; + // public static ClientFoliageManager clientFoliageManager; + public static FoliageCellManager foliageCellManager; //client world data public static ClientWorldData clientWorldData; diff --git a/src/main/java/electrosphere/engine/assetmanager/queue/QueuedTexture.java b/src/main/java/electrosphere/engine/assetmanager/queue/QueuedTexture.java index 23a84cba..a2546a52 100644 --- a/src/main/java/electrosphere/engine/assetmanager/queue/QueuedTexture.java +++ b/src/main/java/electrosphere/engine/assetmanager/queue/QueuedTexture.java @@ -1,6 +1,7 @@ package electrosphere.engine.assetmanager.queue; import java.awt.image.BufferedImage; +import java.nio.ByteBuffer; import electrosphere.engine.Globals; import electrosphere.renderer.texture.Texture; @@ -19,6 +20,21 @@ public class QueuedTexture implements QueuedAsset { //data to be loaded BufferedImage data; + /** + * The byte buffer + */ + ByteBuffer buffer; + + /** + * Width of the image + */ + int width = -1; + + /** + * Height of the image + */ + int height = -1; + /** * Creates the queued texture object @@ -28,9 +44,25 @@ public class QueuedTexture implements QueuedAsset { this.data = image; } + /** + * Creates the queued texture object + * @param buffer The data to buffer + * @param width The width of the buffer + * @param height The height of the buffer + */ + public QueuedTexture(ByteBuffer buffer, int width, int height){ + this.buffer = buffer; + this.width = width; + this.height = height; + } + @Override public void load() { - texture = new Texture(Globals.renderingEngine.getOpenGLState(), data); + if(data != null){ + texture = new Texture(Globals.renderingEngine.getOpenGLState(), data); + } else if(buffer != null){ + texture = new Texture(Globals.renderingEngine.getOpenGLState(),buffer,width,height); + } hasLoaded = true; } @@ -46,6 +78,30 @@ public class QueuedTexture implements QueuedAsset { public Texture getTexture(){ return texture; } + + /** + * Gets the buffer data + * @return The buffer data + */ + public ByteBuffer getBuffer() { + return buffer; + } + + /** + * Gets the width of the buffer + * @return The width + */ + public int getWidth() { + return width; + } + + /** + * Gets the height of the buffer + * @return The height + */ + public int getHeight() { + return height; + } diff --git a/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java b/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java index bc998374..e2eff047 100644 --- a/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java +++ b/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java @@ -8,9 +8,9 @@ import org.joml.Vector3f; import electrosphere.client.entity.camera.CameraEntityUtils; import electrosphere.client.entity.crosshair.Crosshair; import electrosphere.client.fluid.cells.FluidCellManager; -import electrosphere.client.foliagemanager.ClientFoliageManager; import electrosphere.client.sim.ClientSimulation; import electrosphere.client.terrain.cells.ClientDrawCellManager; +import electrosphere.client.terrain.foliage.FoliageCellManager; import electrosphere.client.ui.menu.MenuGenerators; import electrosphere.client.ui.menu.WindowStrings; import electrosphere.client.ui.menu.WindowUtils; @@ -91,10 +91,10 @@ public class ClientLoading { Globals.controlHandler.hintUpdateControlState(ControlHandler.ControlsState.NO_INPUT); //initialize the "real" objects simulation initClientSimulation(); - //init foliage manager - initFoliageManager(); //initialize the cell manager (client) initDrawCellManager(true); + //init foliage manager + initFoliageManager(); //init the fluid cell manager initFluidCellManager(true); //initialize the basic graphical entities of the world (skybox, camera) @@ -369,8 +369,9 @@ public class ClientLoading { * Starts up the foliage manager */ private static void initFoliageManager(){ - Globals.clientFoliageManager = new ClientFoliageManager(); - Globals.clientFoliageManager.start(); + Globals.foliageCellManager = new FoliageCellManager(Globals.clientWorldData.getWorldDiscreteSize()); + Globals.foliageCellManager.init(); + // Globals.foliageCellManager.start(); } diff --git a/src/main/java/electrosphere/engine/threads/ThreadManager.java b/src/main/java/electrosphere/engine/threads/ThreadManager.java index 74e7a71e..be57b3c3 100644 --- a/src/main/java/electrosphere/engine/threads/ThreadManager.java +++ b/src/main/java/electrosphere/engine/threads/ThreadManager.java @@ -6,6 +6,7 @@ import java.util.List; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; +import electrosphere.client.terrain.foliage.FoliageModel; import electrosphere.engine.Globals; import electrosphere.engine.loadingthreads.LoadingThread; import electrosphere.engine.threads.LabeledThread.ThreadLabel; @@ -124,6 +125,7 @@ public class ThreadManager { * Halds all terrain chunk threads */ TerrainChunk.haltThreads(); + FoliageModel.haltThreads(); // //interrupt all threads diff --git a/src/main/java/electrosphere/net/client/protocol/TerrainProtocol.java b/src/main/java/electrosphere/net/client/protocol/TerrainProtocol.java index 1bf0eb1b..4466dc40 100644 --- a/src/main/java/electrosphere/net/client/protocol/TerrainProtocol.java +++ b/src/main/java/electrosphere/net/client/protocol/TerrainProtocol.java @@ -113,7 +113,7 @@ public class TerrainProtocol implements ClientProtocolTemplate { Globals.clientDrawCellManager.markUpdateable(worldPosToUpdate.x, worldPosToUpdate.y, worldPosToUpdate.z); } } - Globals.clientFoliageManager.evaluateChunk(worldPos); + // Globals.clientFoliageManager.evaluateChunk(worldPos); } } break; case SENDFLUIDDATA: { diff --git a/src/main/java/electrosphere/renderer/actor/instance/TextureInstancedActor.java b/src/main/java/electrosphere/renderer/actor/instance/TextureInstancedActor.java index dd0822bf..c0f44af1 100644 --- a/src/main/java/electrosphere/renderer/actor/instance/TextureInstancedActor.java +++ b/src/main/java/electrosphere/renderer/actor/instance/TextureInstancedActor.java @@ -4,6 +4,7 @@ import org.joml.Matrix4d; import org.joml.Vector3d; import electrosphere.engine.Globals; +import electrosphere.engine.assetmanager.queue.QueuedTexture; import electrosphere.entity.Entity; import electrosphere.entity.EntityDataStrings; import electrosphere.renderer.OpenGLState; @@ -27,6 +28,16 @@ public class TextureInstancedActor { //the draw count of the texture instanced actor int drawCount; + /** + * The queued texture + */ + QueuedTexture queuedTexture; + + /** + * Set the queued texture pointer to the material + */ + boolean setQueuedTexturePointer = false; + //shader paths String vertexShaderPath; String fragmentShaderPath; @@ -44,6 +55,19 @@ public class TextureInstancedActor { this.fragmentShaderPath = fragmentShaderPath; } + /** + * Creates an instanced actor + * @param modelPath The path of the model this actor uses + */ + protected TextureInstancedActor(String modelPath, String vertexShaderPath, String fragmentShaderPath, QueuedTexture dataTexture, int drawCount){ + this.modelPath = modelPath; + this.material = new Material(); + this.queuedTexture = dataTexture; + this.drawCount = drawCount; + this.vertexShaderPath = vertexShaderPath; + this.fragmentShaderPath = fragmentShaderPath; + } + /** * Attaches a TextureInstancedActor to an entity * @param parent The entity @@ -54,6 +78,17 @@ public class TextureInstancedActor { TextureInstancedActor newActor = new TextureInstancedActor(modelPath, vertexShaderPath, fragmentShaderPath, dataTexture, drawCount); parent.putData(EntityDataStrings.TEXTURE_INSTANCED_ACTOR, newActor); } + + /** + * Attaches a TextureInstancedActor to an entity + * @param parent The entity + * @param modelPath The path to the model for this instanced actor + * @param dataTexture The data texture containing data for this actor + */ + public static void attachTextureInstancedActor(Entity parent, String modelPath, String vertexShaderPath, String fragmentShaderPath, QueuedTexture dataTexture, int drawCount){ + TextureInstancedActor newActor = new TextureInstancedActor(modelPath, vertexShaderPath, fragmentShaderPath, dataTexture, drawCount); + parent.putData(EntityDataStrings.TEXTURE_INSTANCED_ACTOR, newActor); + } /** * Draws the instanced actor. Should be called normally in a loop as if this was a regular actor. @@ -63,7 +98,21 @@ public class TextureInstancedActor { public void draw(RenderPipelineState renderPipelineState, OpenGLState openGLState){ Model model = Globals.assetManager.fetchModel(modelPath); VisualShader shader = Globals.assetManager.fetchShader(vertexShaderPath, fragmentShaderPath); - if(model != null && shader != null){ + if(queuedTexture != null && !setQueuedTexturePointer && queuedTexture.getTexture() != null){ + this.material.setTexturePointer(queuedTexture.getTexture().getTexturePointer()); + setQueuedTexturePointer = true; + } + if( + model != null && + shader != null && + ( + queuedTexture == null || + ( + queuedTexture != null && + queuedTexture.getTexture() != null + ) + ) + ){ //setup render pipeline boolean instancedState = renderPipelineState.getInstanced(); boolean materialState = renderPipelineState.getUseMaterial(); diff --git a/src/main/java/electrosphere/renderer/pipelines/MainContentPipeline.java b/src/main/java/electrosphere/renderer/pipelines/MainContentPipeline.java index ee27156f..a43cb1b9 100644 --- a/src/main/java/electrosphere/renderer/pipelines/MainContentPipeline.java +++ b/src/main/java/electrosphere/renderer/pipelines/MainContentPipeline.java @@ -89,7 +89,7 @@ public class MainContentPipeline implements RenderPipeline { currentActor.draw(renderPipelineState,openGLState); } } - Globals.clientFoliageManager.draw(); + Globals.foliageCellManager.draw(); for(Entity currentEntity : Globals.clientScene.getEntitiesWithTag(EntityTags.DRAW_INSTANCED)){ Vector3d position = EntityUtils.getPosition(currentEntity); if(shouldDrawSolidPass(currentEntity)){