diff --git a/docs/src/progress/renderertodo.md b/docs/src/progress/renderertodo.md index f4bf22d7..0934f28f 100644 --- a/docs/src/progress/renderertodo.md +++ b/docs/src/progress/renderertodo.md @@ -1149,6 +1149,7 @@ Refactoring server side of terrain management Add server manager for block chunk data & management Add endpoint to request strided block data Add client side handling of block endpoint +Add server-driven block rasterizer with LOD # TODO diff --git a/src/main/java/electrosphere/client/block/BlockChunkData.java b/src/main/java/electrosphere/client/block/BlockChunkData.java index 7bd53ce8..a57ad7d5 100644 --- a/src/main/java/electrosphere/client/block/BlockChunkData.java +++ b/src/main/java/electrosphere/client/block/BlockChunkData.java @@ -44,6 +44,31 @@ public class BlockChunkData { */ public static final int LOD_FULL_RES = 0; + /** + * Lod value for a half res chunk + */ + public static final int LOD_HALF_RES = 1; + + /** + * Lod value for a quarter res chunk + */ + public static final int LOD_QUARTER_RES = 2; + + /** + * Lod value for a eighth res chunk + */ + public static final int LOD_EIGHTH_RES = 3; + + /** + * Lod value for a sixteenth res chunk + */ + public static final int LOD_SIXTEENTH_RES = 4; + + /** + * Lod value for the lowest resolution possible + */ + public static final int LOD_LOWEST_RES = LOD_SIXTEENTH_RES; + /** * The type of block at a given position @@ -299,4 +324,12 @@ public class BlockChunkData { this.homogenousValue = homogenousValue; } + /** + * Sets the homogenous value + * @param homogenousValue The homogenous value + */ + public void setHomogenousValue(int homogenousValue){ + this.setHomogenousValue((short)homogenousValue); + } + } diff --git a/src/main/java/electrosphere/client/block/cells/BlockDrawCell.java b/src/main/java/electrosphere/client/block/cells/BlockDrawCell.java new file mode 100644 index 00000000..b8c4879a --- /dev/null +++ b/src/main/java/electrosphere/client/block/cells/BlockDrawCell.java @@ -0,0 +1,315 @@ +package electrosphere.client.block.cells; + +import org.joml.Quaterniond; +import org.joml.Vector3d; +import org.joml.Vector3i; + +import electrosphere.client.block.BlockChunkData; +import electrosphere.engine.Globals; +import electrosphere.entity.ClientEntityUtils; +import electrosphere.entity.Entity; +import electrosphere.entity.btree.BehaviorTree; +import electrosphere.entity.types.terrain.BlockChunkEntity; +import electrosphere.server.terrain.manager.ServerTerrainChunk; +import electrosphere.util.ds.octree.WorldOctTree.WorldOctTreeNode; +import electrosphere.util.math.GeomUtils; + +/** + * A single drawcell - contains an entity that has a physics mesh and potentially graphics + */ +public class BlockDrawCell { + + /** + * 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; + + //the position of the draw cell in world coordinates + Vector3i worldPos; + + /** + * The LOD of the draw cell + */ + int lod; + + //the main entity for the cell + Entity modelEntity; + + /** + * The data for generating the visuals + */ + BlockChunkData chunkData; + + /** + * Tracks whether the draw cell has requested its chunk data or not + */ + boolean hasRequested = false; + + /** + * Tracks whether the draw cell has generated its entity or not + */ + boolean hasGenerated = false; + + + /** + * Tracks whether this draw 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 + */ + BlockDrawCell notifyTarget = null; + + /** + * The number of cells that have alerted this one + */ + int generationAlertCount = 0; + + + /** + * Private constructor + */ + private BlockDrawCell(){ + + } + + + /** + * Constructs a drawcell object + */ + public static BlockDrawCell generateBlockCell( + Vector3i worldPos, + int lod + ){ + BlockDrawCell rVal = new BlockDrawCell(); + rVal.lod = lod; + rVal.worldPos = worldPos; + return rVal; + } + + /** + * Generates a drawable entity based on this chunk + */ + public void generateDrawableEntity(BlockTextureAtlas atlas, int lod){ + boolean success = true; + if(chunkData == null){ + BlockChunkData currentChunk = Globals.clientBlockManager.getChunkDataAtWorldPoint( + worldPos.x, + worldPos.y, + worldPos.z, + lod + ); + if(currentChunk == null){ + success = false; + } else { + this.homogenous = currentChunk.getHomogenousValue() != BlockChunkData.NOT_HOMOGENOUS; + success = true; + } + if(!success){ + this.setFailedGenerationAttempts(this.getFailedGenerationAttempts() + 1); + return; + } + this.chunkData = currentChunk; + } + Entity toDelete = this.modelEntity; + modelEntity = BlockChunkEntity.clientCreateBlockChunkEntity(chunkData, notifyTarget, toDelete, lod, atlas, this.hasPolygons()); + ClientEntityUtils.initiallyPositionEntity(modelEntity, this.getRealPos(), new Quaterniond()); + this.setHasGenerated(true); + } + + /** + * Gets the real-space position of the draw cell + * @return the real-space position + */ + protected Vector3d getRealPos(){ + return new Vector3d( + worldPos.x * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET, + worldPos.y * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET, + worldPos.z * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET + ); + } + + /** + * Gets the world-space position of the draw cell + * @return the world-space position + */ + protected Vector3i getWorldPos(){ + return new Vector3i(worldPos); + } + + /** + * Registers a target draw cell to notify once this one has completed generating its model + * @param notifyTarget The target to notify + */ + public void registerNotificationTarget(BlockDrawCell notifyTarget){ + this.notifyTarget = notifyTarget; + } + + /** + * Alerts this draw 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 drawcell 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 draw cell + * @param source The source draw cell + */ + public void transferChunkData(BlockDrawCell source){ + this.chunkData = source.chunkData; + this.homogenous = source.homogenous; + this.hasRequested = source.hasRequested; + } + + /** + * Gets whether this draw cell has requested its chunk data or not + * @return true if has requested, false otherwise + */ + public boolean hasRequested() { + return hasRequested; + } + + /** + * Sets whether this draw 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 draw cell has generated its entity or not + * @return true if has generated, false otherwise + */ + public boolean hasGenerated() { + return hasGenerated; + } + + /** + * Sets whether this draw 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 draw cell is homogenous or not + * @param hasGenerated true if is homogenous, false otherwise + */ + public void setHomogenous(boolean homogenous) { + this.homogenous = homogenous; + } + + /** + * Gets whether this draw cell will generate polygons or not + * @return true if it has polygons, false otherwise + */ + private boolean hasPolygons(){ + return !this.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; + } + + /** + * Ejects the chunk data + */ + public void ejectChunkData(){ + this.chunkData = null; + } + + /** + * Gets whether this draw 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/block/cells/BlockTextureAtlas.java b/src/main/java/electrosphere/client/block/cells/BlockTextureAtlas.java new file mode 100644 index 00000000..0f48c9cb --- /dev/null +++ b/src/main/java/electrosphere/client/block/cells/BlockTextureAtlas.java @@ -0,0 +1,80 @@ +package electrosphere.client.block.cells; + +import java.util.HashMap; +import java.util.Map; + +import electrosphere.renderer.texture.Texture; + +/** + * An atlas texture and accompanying map of all voxel textures + */ +public class BlockTextureAtlas { + + //A map of voxel id -> coordinates in the atlas texture for its texture + Map typeCoordMap = new HashMap(); + + + //the actual texture + Texture specular; + + //the normal texture + Texture normal; + + //the width in pixels of a single texture in the atlas + public static final int ATLAS_ELEMENT_DIM = 256; + //the width in pixels of the whole atlas texture + public static final int ATLAS_DIM = 8192; + //number of textures per row in the atlas + public static final int ELEMENTS_PER_ROW = ATLAS_DIM / ATLAS_ELEMENT_DIM; + + /** + * Puts an entry in the type-coord map to map a voxel type to a position + * @param type the voxel type + * @param coord the coordinate in the map + */ + public void putTypeCoord(int type, int coord){ + typeCoordMap.put(type,coord); + } + + /** + * Sets the specular + * @param specular the specular + */ + public void setSpecular(Texture specular){ + this.specular = specular; + } + + /** + * Sets the normal + * @param normal the normal + */ + public void setNormal(Texture normal){ + this.normal = normal; + } + + /** + * Gets the atlas specular + * @return the atlas specular + */ + public Texture getSpecular(){ + return specular; + } + + /** + * Gets the atlas normal + * @return the atlas normal + */ + public Texture getNormal(){ + return normal; + } + + /** + * Gets the index in the atlas of a provided voxel type (the voxel type is provided by its id) + * @param voxelTypeId The id of the voxel type + * @return the index in the atlas of the texture of the provided voxel type + */ + public int getVoxelTypeOffset(int voxelTypeId){ + return typeCoordMap.containsKey(voxelTypeId) ? typeCoordMap.get(voxelTypeId) : -1; + } + +} diff --git a/src/main/java/electrosphere/client/block/cells/ClientBlockCellManager.java b/src/main/java/electrosphere/client/block/cells/ClientBlockCellManager.java new file mode 100644 index 00000000..b4271211 --- /dev/null +++ b/src/main/java/electrosphere/client/block/cells/ClientBlockCellManager.java @@ -0,0 +1,838 @@ +package electrosphere.client.block.cells; + +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.client.block.BlockChunkData; +import electrosphere.collision.PhysicsEntityUtils; +import electrosphere.engine.Globals; +import electrosphere.entity.EntityUtils; +import electrosphere.logger.LoggerInterface; +import electrosphere.util.ds.octree.WorldOctTree; +import electrosphere.util.ds.octree.WorldOctTree.WorldOctTreeNode; +import electrosphere.util.math.GeomUtils; + +/** + * Manages draw cells on the client + */ +public class ClientBlockCellManager { + + /** + * Number of times to try updating per frame. Lower this to reduce lag but slow down block 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 draw at full resolution + */ + public static final double FULL_RES_DIST = 8 * 8; + + /** + * The distance for half resolution + */ + public static final double HALF_RES_DIST = 16 * 16; + + /** + * The distance for quarter resolution + */ + public static final double QUARTER_RES_DIST = 20 * 20; + + /** + * 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 = 64 * 64; + + /** + * 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 client draw cell manager should update or not + */ + boolean shouldUpdate = true; + + /** + * The voxel texture atlas + */ + BlockTextureAtlas textureAtlas; + + /** + * 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 voxelTextureAtlas The voxel texture atlas + * @param worldDim The size of the world in chunks + */ + public ClientBlockCellManager(BlockTextureAtlas voxelTextureAtlas, int worldDim){ + this.chunkTree = new WorldOctTree( + new Vector3i(0,0,0), + new Vector3i(worldDim, worldDim, worldDim) + ); + this.chunkTree.getRoot().setData(BlockDrawCell.generateBlockCell(new Vector3i(0,0,0), chunkTree.getMaxLevel())); + this.worldDim = worldDim; + this.textureAtlas = voxelTextureAtlas; + } + + /** + * Updates all cells in the chunk + */ + public void update(){ + Globals.profiler.beginCpuSample("ClientDrawCellManager.update"); + if(shouldUpdate && Globals.playerEntity != null){ + Vector3d playerPos = EntityUtils.getPosition(Globals.playerEntity); + Vector3i playerWorldPos = Globals.clientWorldData.convertRealToWorldSpace(playerPos); + int distCache = this.getDistCache(this.lastPlayerPos, playerWorldPos); + this.lastPlayerPos.set(playerWorldPos); + //the sets to iterate through + updatedLastFrame = true; + validCellCount = 0; + evaluationMap.clear(); + //update all full res cells + WorldOctTreeNode rootNode = this.chunkTree.getRoot(); + Globals.profiler.beginCpuSample("ClientDrawCellManager.update - full res cells"); + updatedLastFrame = this.recursivelyUpdateCells(rootNode, playerWorldPos, evaluationMap, BlockChunkData.LOD_LOWEST_RES, distCache); + Globals.profiler.endCpuSample(); + if(!updatedLastFrame && !this.initialized){ + this.initialized = true; + } + } + Globals.profiler.endCpuSample(); + } + + /** + * Recursively update child nodes + * @param node The root node + * @param playerPos 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 playerPos, 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(playerPos, node, distCache)){ + Globals.profiler.beginCpuSample("ClientDrawCellManager.split"); + //perform op + WorldOctTreeNode container = chunkTree.split(node); + BlockDrawCell containerCell = BlockDrawCell.generateBlockCell(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 + ); + BlockDrawCell drawCell = BlockDrawCell.generateBlockCell(cellWorldPos,this.chunkTree.getMaxLevel() - child.getLevel()); + drawCell.registerNotificationTarget(node.getData()); + child.setLeaf(true); + child.setData(drawCell); + 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(playerPos, node, minLeafLod, distCache)){ + Globals.profiler.beginCpuSample("ClientDrawCellManager.request"); + + //calculate what to request + BlockDrawCell 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(playerPos, node, minLeafLod, distCache)){ + Globals.profiler.beginCpuSample("ClientDrawCellManager.generate"); + int lodLevel = this.getLODLevel(node); + + if(this.containsDataToGenerate(node)){ + node.getData().generateDrawableEntity(textureAtlas, 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(playerPos, 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, playerPos, 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; + } + + /** + * 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 BlockChunkData.LOD_SIXTEENTH_RES + 2; + } + if( + lastPlayerPos.x / 8 != currentPlayerPos.x / 8 || lastPlayerPos.z / 8 != currentPlayerPos.z / 8 || lastPlayerPos.z / 8 != currentPlayerPos.z / 8 + ){ + return BlockChunkData.LOD_SIXTEENTH_RES + 1; + } + if( + lastPlayerPos.x / 4 != currentPlayerPos.x / 4 || lastPlayerPos.z / 4 != currentPlayerPos.z / 4 || lastPlayerPos.z / 4 != currentPlayerPos.z / 4 + ){ + return BlockChunkData.LOD_SIXTEENTH_RES; + } + if( + lastPlayerPos.x / 2 != currentPlayerPos.x / 2 || lastPlayerPos.z / 2 != currentPlayerPos.z / 2 || lastPlayerPos.z / 2 != currentPlayerPos.z / 2 + ){ + return BlockChunkData.LOD_EIGHTH_RES; + } + if( + lastPlayerPos.x != currentPlayerPos.x || lastPlayerPos.z != currentPlayerPos.z || lastPlayerPos.z != currentPlayerPos.z + ){ + return BlockChunkData.LOD_QUARTER_RES; + } + 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 draw calls + return + node.canSplit() && + (node.getLevel() != this.chunkTree.getMaxLevel()) && + !node.getData().isHomogenous() && + (node.getParent() != null || node == this.chunkTree.getRoot()) && + ( + ( + node.getLevel() < this.chunkTree.getMaxLevel() - BlockChunkData.LOD_SIXTEENTH_RES && + this.getMinDistance(pos, node, distCache) <= SIXTEENTH_RES_DIST + ) + || + ( + node.getLevel() < this.chunkTree.getMaxLevel() - BlockChunkData.LOD_EIGHTH_RES && + this.getMinDistance(pos, node, distCache) <= EIGHTH_RES_DIST + ) + || + ( + node.getLevel() < this.chunkTree.getMaxLevel() - BlockChunkData.LOD_QUARTER_RES && + this.getMinDistance(pos, node, distCache) <= QUARTER_RES_DIST + ) + || + ( + node.getLevel() < this.chunkTree.getMaxLevel() - BlockChunkData.LOD_HALF_RES && + 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 draw cell + * @param node The node to consider + * @return -1 if outside of render range, -1 if the node is not a valid draw 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 > BlockChunkData.LOD_FULL_RES){ + 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 draw calls + return + node.getLevel() > 0 && + (node.getLevel() != this.chunkTree.getMaxLevel()) && + ( + ( + node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_HALF_RES && + this.getMinDistance(pos, node, distCache) > FULL_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_QUARTER_RES && + this.getMinDistance(pos, node, distCache) > HALF_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_EIGHTH_RES && + this.getMinDistance(pos, node, distCache) > QUARTER_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_SIXTEENTH_RES && + 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("ClientDrawCellManager.join"); + + //queue destructions prior to join -- the join operator clears all children on node + this.recursivelyDestroy(node); + + //perform op + BlockDrawCell newLeafCell = BlockDrawCell.generateBlockCell(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() - BlockChunkData.LOD_HALF_RES + && + this.getMinDistance(pos, node, distCache) <= QUARTER_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_QUARTER_RES + && + this.getMinDistance(pos, node, distCache) <= EIGHTH_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_EIGHTH_RES + && + this.getMinDistance(pos, node, distCache) <= SIXTEENTH_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_SIXTEENTH_RES + && + 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() - BlockChunkData.LOD_HALF_RES + && + this.getMinDistance(pos, node, distCache) <= QUARTER_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_QUARTER_RES + && + this.getMinDistance(pos, node, distCache) <= EIGHTH_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_EIGHTH_RES + && + this.getMinDistance(pos, node, distCache) <= SIXTEENTH_RES_DIST + ) + || + ( + node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_SIXTEENTH_RES + && + 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 draw cell as updateable + * @param worldX The world x position + * @param worldY The world y position + * @param worldZ The world z position + */ + public void markUpdateable(int worldX, int worldY, int worldZ){ + BlockDrawCell drawCell = this.getDrawCell(worldX, worldY, worldZ); + drawCell.ejectChunkData(); + drawCell.setHasGenerated(false); + drawCell.setHasRequested(false); + } + + /** + * Requests all chunks for a given draw cell + * @param cell The cell + * @return true if all cells were successfully requested, false otherwise + */ + private boolean requestChunks(WorldOctTree.WorldOctTreeNode node){ + int lod = this.chunkTree.getMaxLevel() - node.getLevel(); + 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.clientBlockManager.containsChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, lod) + ){ + //client should request chunk data from server for each chunk necessary to create the model + LoggerInterface.loggerNetworking.DEBUG("(Client) Send Request for block data at " + worldPos); + if(!Globals.clientBlockManager.requestChunk(worldPos.x, worldPos.y, worldPos.z, lod)){ + return false; + } + } + return true; + } + + /** + * Checks if all chunk data required to generate this draw cell is present + * @param node The node + * @return true if all data is available, false otherwise + */ + private boolean containsDataToGenerate(WorldOctTree.WorldOctTreeNode node){ + BlockDrawCell cell = node.getData(); + int lod = this.chunkTree.getMaxLevel() - node.getLevel(); + Vector3i worldPos = cell.getWorldPos(); + if(!Globals.clientBlockManager.containsChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, lod)){ + return false; + } + return true; + } + + /** + * Sets whether the draw 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 draw 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 draw 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 draw cell manager has initialized or not + * @return true if it has initialized, false otherwise + */ + public boolean isInitialized(){ + return this.initialized; + } + + /** + * Gets the draw 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 draw cell if it exists, null otherwise + */ + public BlockDrawCell getDrawCell(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; + } + + /** + * Checks if physics has been generated for a given world coordinate + * @param worldX The world x coordinate + * @param worldY The world y coordinate + * @param worldZ The world z coordinate + * @return true if physics has been generated, false otherwise + */ + public boolean hasGeneratedPhysics(int worldX, int worldY, int worldZ){ + BlockDrawCell cell = this.getDrawCell(worldX, worldY, worldZ); + if(cell != null && cell.getEntity() != null){ + return PhysicsEntityUtils.containsDBody(cell.getEntity()); + } + return false; + } + + + +} diff --git a/src/main/java/electrosphere/client/sim/ClientSimulation.java b/src/main/java/electrosphere/client/sim/ClientSimulation.java index 81dc6ed5..c5ebc37c 100644 --- a/src/main/java/electrosphere/client/sim/ClientSimulation.java +++ b/src/main/java/electrosphere/client/sim/ClientSimulation.java @@ -209,7 +209,9 @@ public class ClientSimulation { * Updates the block cell manager */ private void updateBlockCellManager(){ - + if(Globals.clientBlockCellManager != null && Globals.clientWorldData != null){ + Globals.clientBlockCellManager.update(); + } } /** diff --git a/src/main/java/electrosphere/engine/Globals.java b/src/main/java/electrosphere/engine/Globals.java index 82cdae57..9273c2b0 100644 --- a/src/main/java/electrosphere/engine/Globals.java +++ b/src/main/java/electrosphere/engine/Globals.java @@ -14,6 +14,8 @@ import electrosphere.audio.collision.HitboxAudioService; import electrosphere.audio.movement.MovementAudioService; import electrosphere.auth.AuthenticationManager; import electrosphere.client.block.ClientBlockManager; +import electrosphere.client.block.cells.BlockTextureAtlas; +import electrosphere.client.block.cells.ClientBlockCellManager; import electrosphere.client.chemistry.ClientChemistryCollisionCallback; import electrosphere.client.entity.particle.ParticleService; import electrosphere.client.fluid.cells.FluidCellManager; @@ -362,6 +364,8 @@ public class Globals { //draw cell manager public static ClientDrawCellManager clientDrawCellManager; public static VoxelTextureAtlas voxelTextureAtlas = new VoxelTextureAtlas(); + public static ClientBlockCellManager clientBlockCellManager; + public static BlockTextureAtlas blockTextureAtlas = null; //fluid cell manager public static FluidCellManager fluidCellManager; diff --git a/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java b/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java index 408aef8e..807f63ba 100644 --- a/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java +++ b/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java @@ -5,7 +5,7 @@ import java.util.concurrent.TimeUnit; import org.joml.Vector3f; -import electrosphere.client.block.BlockChunkData; +import electrosphere.client.block.cells.ClientBlockCellManager; import electrosphere.client.entity.camera.CameraEntityUtils; import electrosphere.client.entity.crosshair.Crosshair; import electrosphere.client.fluid.cells.FluidCellManager; @@ -19,7 +19,6 @@ import electrosphere.client.ui.menu.mainmenu.MenuCharacterCreation; import electrosphere.controls.ControlHandler; import electrosphere.engine.Globals; import electrosphere.engine.assetmanager.AssetDataStrings; -import electrosphere.engine.assetmanager.queue.QueuedModel; import electrosphere.engine.signal.Signal.SignalType; import electrosphere.engine.threads.LabeledThread.ThreadLabel; import electrosphere.entity.DrawableUtils; @@ -31,8 +30,6 @@ import electrosphere.net.NetUtils; import electrosphere.net.client.ClientNetworking; import electrosphere.renderer.actor.Actor; import electrosphere.renderer.actor.ActorTextureMask; -import electrosphere.renderer.meshgen.BlockMeshgen; -import electrosphere.renderer.meshgen.BlockMeshgen.BlockMeshData; public class ClientLoading { @@ -95,12 +92,11 @@ public class ClientLoading { Globals.controlHandler.hintUpdateControlState(ControlHandler.ControlsState.NO_INPUT); //initialize the "real" objects simulation initClientSimulation(); - //initialize the cell manager (client) + //initialize the gridded managers (client) initDrawCellManager(true); - //init foliage manager initFoliageManager(); - //init the fluid cell manager initFluidCellManager(true); + initBlockCellManager(true); //initialize the basic graphical entities of the world (skybox, camera) initWorldBaseGraphicalEntities(); //init arena specific stuff (ie different skybox colors) @@ -141,11 +137,11 @@ public class ClientLoading { Globals.cameraHandler.setUpdate(false); //initialize the "real" objects simulation initClientSimulation(); - //init foliage manager - initFoliageManager(); //initialize the cell managers (client) initDrawCellManager(false); initFluidCellManager(false); + initBlockCellManager(false); + initFoliageManager(); //sets micro and macro sims to ready if they exist setSimulationsToReady(); @@ -276,23 +272,6 @@ public class ClientLoading { cursorActor.addTextureMask(new ActorTextureMask("sphere", Arrays.asList(new String[]{"Textures/transparent_red.png"}))); DrawableUtils.makeEntityTransparent(Globals.playerCursor); EntityUtils.getScale(Globals.playerCursor).set(0.2f); - - //block test - Entity blockEntity = EntityCreationUtils.createClientSpatialEntity(); - BlockChunkData blockChunkData = BlockChunkData.allocate(); - blockChunkData.setType(0, 0, 0, 1); - blockChunkData.setType(1, 0, 0, 1); - blockChunkData.setType(0, 1, 0, 1); - blockChunkData.setType(1, 1, 0, 1); - blockChunkData.setType(0, 0, 1, 1); - blockChunkData.setType(1, 0, 1, 1); - blockChunkData.setType(0, 1, 1, 1); - blockChunkData.setType(1, 1, 1, 1); - BlockMeshData meshData = BlockMeshgen.rasterize(blockChunkData); - String modelPath = Globals.assetManager.queuedAsset(new QueuedModel(() -> { - return BlockMeshgen.generateBlockModel(meshData); - })); - EntityCreationUtils.makeEntityDrawablePreexistingModel(blockEntity, modelPath); } static final int MAX_DRAW_CELL_WAIT = 1000; @@ -386,6 +365,52 @@ public class ClientLoading { } } + /** + * Inits the block cell manager + * @param blockForInit Blocks the thread until the block cell manager is ready + */ + static void initBlockCellManager(boolean blockForInit){ + int iterations = 0; + WindowUtils.updateLoadingWindow("WAITING ON WORLD DATA"); + while(blockForInit && (Globals.clientWorldData == null || InitialAssetLoading.atlasQueuedTexture == null || !InitialAssetLoading.atlasQueuedTexture.hasLoaded()) && Globals.threadManager.shouldKeepRunning()){ + try { + TimeUnit.MILLISECONDS.sleep(10); + iterations++; + } catch (InterruptedException ex) { + LoggerInterface.loggerEngine.ERROR(ex); + } + if(iterations > MAX_DRAW_CELL_WAIT){ + String message = "Draw cell took too long to init!\n" + + Globals.clientWorldData + "\n" + + InitialAssetLoading.atlasQueuedTexture.hasLoaded(); + throw new IllegalStateException(message); + } + } + Globals.clientBlockCellManager = new ClientBlockCellManager(Globals.blockTextureAtlas, Globals.clientWorldData.getWorldDiscreteSize()); + //Alerts the client simulation that it should start loading blocks + Globals.clientSimulation.setLoadingTerrain(true); + //wait for all the block data to arrive + int i = 0; + while( + blockForInit && + !Globals.clientBlockCellManager.isInitialized() && + Globals.threadManager.shouldKeepRunning() + ){ + i++; + if(i % DRAW_CELL_UPDATE_RATE == 0){ + WindowUtils.updateLoadingWindow("WAITING ON SERVER TO SEND BLOCKS (" + Globals.clientTerrainManager.getAllChunks().size() + ")"); + } + try { + TimeUnit.MILLISECONDS.sleep(10); + } catch (InterruptedException ex) { + ex.printStackTrace(); + } + } + if(i < DRAW_CELL_EXPECTED_MINIMUM_FRAMES_TO_INIT){ + LoggerInterface.loggerEngine.WARNING("Block cell manager loaded exceptionally fast!"); + } + } + /** * Starts up the foliage manager */ diff --git a/src/main/java/electrosphere/engine/threads/ThreadManager.java b/src/main/java/electrosphere/engine/threads/ThreadManager.java index be57b3c3..e09aff98 100644 --- a/src/main/java/electrosphere/engine/threads/ThreadManager.java +++ b/src/main/java/electrosphere/engine/threads/ThreadManager.java @@ -10,6 +10,7 @@ import electrosphere.client.terrain.foliage.FoliageModel; import electrosphere.engine.Globals; import electrosphere.engine.loadingthreads.LoadingThread; import electrosphere.engine.threads.LabeledThread.ThreadLabel; +import electrosphere.entity.types.terrain.BlockChunkEntity; import electrosphere.entity.types.terrain.TerrainChunk; import electrosphere.server.datacell.Realm; import electrosphere.util.CodeUtils; @@ -118,6 +119,9 @@ public class ThreadManager { if(realm.getServerWorldData() != null && realm.getServerWorldData().getServerTerrainManager() != null){ realm.getServerWorldData().getServerTerrainManager().closeThreads(); } + if(realm.getServerWorldData() != null && realm.getServerWorldData().getServerBlockManager() != null){ + realm.getServerWorldData().getServerBlockManager().closeThreads(); + } } } @@ -126,6 +130,7 @@ public class ThreadManager { */ TerrainChunk.haltThreads(); FoliageModel.haltThreads(); + BlockChunkEntity.haltThreads(); // //interrupt all threads diff --git a/src/main/java/electrosphere/entity/types/terrain/BlockChunkEntity.java b/src/main/java/electrosphere/entity/types/terrain/BlockChunkEntity.java new file mode 100644 index 00000000..11ee589e --- /dev/null +++ b/src/main/java/electrosphere/entity/types/terrain/BlockChunkEntity.java @@ -0,0 +1,109 @@ +package electrosphere.entity.types.terrain; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.joml.Quaterniond; +import org.joml.Vector3d; + +import electrosphere.client.block.BlockChunkData; +import electrosphere.client.block.cells.BlockDrawCell; +import electrosphere.client.block.cells.BlockTextureAtlas; +import electrosphere.engine.Globals; +import electrosphere.engine.assetmanager.queue.QueuedModel; +import electrosphere.entity.ClientEntityUtils; +import electrosphere.entity.Entity; +import electrosphere.entity.EntityCreationUtils; +import electrosphere.entity.EntityDataStrings; +import electrosphere.entity.EntityUtils; +import electrosphere.entity.types.collision.CollisionObjUtils; +import electrosphere.logger.LoggerInterface; +import electrosphere.renderer.meshgen.BlockMeshgen; +import electrosphere.renderer.meshgen.BlockMeshgen.BlockMeshData; + +/** + * Generates block chunk entities + */ +public class BlockChunkEntity { + + /** + * Used for generating block chunks + */ + static final ExecutorService generationService = Executors.newFixedThreadPool(4); + + /** + * Creates a client block chunk based on weights and values provided + * @param chunkData the chunk data to generate with + * @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 hasPolygons true if the chunk has polygons to generate a model with, false otherwise + * @return The block chunk entity + */ + public static Entity clientCreateBlockChunkEntity( + BlockChunkData chunkData, + BlockDrawCell notifyTarget, + Entity toDelete, + int levelOfDetail, + BlockTextureAtlas atlas, + boolean hasPolygons + ){ + Globals.profiler.beginAggregateCpuSample("BlockChunk.clientCreateBlockChunkEntity"); + + Entity rVal = EntityCreationUtils.createClientSpatialEntity(); + + if(hasPolygons && chunkData.getType() != null && chunkData.getMetadata() != null){ + generationService.submit(() -> { + BlockMeshData data; + try { + data = BlockMeshgen.rasterize(chunkData); + if(Globals.clientScene.containsEntity(rVal)){ + String modelPath = Globals.assetManager.queuedAsset(new QueuedModel(() -> { + return BlockMeshgen.generateBlockModel(data); + })); + EntityCreationUtils.makeEntityDrawablePreexistingModel(rVal, modelPath); + if(levelOfDetail == BlockChunkData.LOD_FULL_RES){ + // PhysicsEntityUtils.clientAttachTerrainChunkRigidBody(rVal, data); + CollisionObjUtils.clientPositionCharacter(rVal, new Vector3d(EntityUtils.getPosition(rVal)), new Quaterniond()); + } else { + EntityCreationUtils.bypassShadowPass(rVal); + EntityCreationUtils.bypassVolumetics(rVal); + } + rVal.putData(EntityDataStrings.HAS_UNIQUE_MODEL, true); + } else { + if(notifyTarget != null){ + notifyTarget.alertToGeneration(); + } + if(toDelete != null){ + ClientEntityUtils.destroyEntity(toDelete); + } + LoggerInterface.loggerEngine.DEBUG("Finished generating block polygons; however, entity has already been deleted."); + } + } catch (Error e){ + LoggerInterface.loggerEngine.ERROR(e); + } catch(Exception e){ + LoggerInterface.loggerEngine.ERROR(e); + } + }); + } else { + if(notifyTarget != null){ + notifyTarget.alertToGeneration(); + } + if(toDelete != null){ + ClientEntityUtils.destroyEntity(toDelete); + } + } + + rVal.putData(EntityDataStrings.TERRAIN_IS_TERRAIN, true); + Globals.profiler.endCpuSample(); + return rVal; + } + + /** + * Halts all running generation threads + */ + public static void haltThreads(){ + generationService.shutdownNow(); + } + +} diff --git a/src/main/java/electrosphere/server/block/manager/ServerBlockChunkGenerationThread.java b/src/main/java/electrosphere/server/block/manager/ServerBlockChunkGenerationThread.java index 07340c1e..e0901e5a 100644 --- a/src/main/java/electrosphere/server/block/manager/ServerBlockChunkGenerationThread.java +++ b/src/main/java/electrosphere/server/block/manager/ServerBlockChunkGenerationThread.java @@ -104,6 +104,17 @@ public class ServerBlockChunkGenerationThread implements Runnable { if(chunk == null){ //TODO: generate from macro-level data chunk = BlockChunkData.allocate(); + chunk.setHomogenousValue(0); + if(worldX == 0 && worldY == 0 && worldZ == 0){ + chunk.setHomogenousValue(BlockChunkData.NOT_HOMOGENOUS); + for(int x = 0; x < 16; x++){ + for(int y = 0; y < 16; y++){ + for(int z = 0; z < 16; z++){ + chunk.setType(x, y, z, 1); + } + } + } + } } if(chunk != null){ chunkCache.add(worldX, worldY, worldZ, stride, chunk); diff --git a/src/main/java/electrosphere/server/block/manager/ServerBlockManager.java b/src/main/java/electrosphere/server/block/manager/ServerBlockManager.java index 33d551ee..71b7ca7e 100644 --- a/src/main/java/electrosphere/server/block/manager/ServerBlockManager.java +++ b/src/main/java/electrosphere/server/block/manager/ServerBlockManager.java @@ -120,6 +120,7 @@ public class ServerBlockManager { returnedChunk.setWorldX(worldX); returnedChunk.setWorldY(worldY); returnedChunk.setWorldZ(worldZ); + returnedChunk.setHomogenousValue(0); } this.chunkCache.add(worldX, worldY, worldZ, BlockChunkData.LOD_FULL_RES, returnedChunk); }