From a44255060d38867e82b6d2f118b54d041b3527ab Mon Sep 17 00:00:00 2001 From: austin Date: Sun, 17 Nov 2024 15:40:59 -0500 Subject: [PATCH] Voxel system improvements --- docs/src/progress/renderertodo.md | 4 ++ .../terrain/cells/ClientDrawCellManager.java | 56 ++++++++++++------- .../client/terrain/cells/DrawCell.java | 43 +++++++++++--- .../terrain/manager/ClientTerrainManager.java | 13 ++++- .../manager/TerrainChunkGenQueueItem.java | 23 +++++++- .../engine/loadingthreads/ClientLoading.java | 3 + .../entity/EntityDataStrings.java | 1 + .../electrosphere/entity/scene/Scene.java | 10 ++++ .../entity/types/terrain/TerrainChunk.java | 28 +++++++++- 9 files changed, 150 insertions(+), 31 deletions(-) diff --git a/docs/src/progress/renderertodo.md b/docs/src/progress/renderertodo.md index 7bc1de8e..ce96db24 100644 --- a/docs/src/progress/renderertodo.md +++ b/docs/src/progress/renderertodo.md @@ -1061,6 +1061,10 @@ Fix hitbox destruction logic to not double-delete (11/17/2024) Mountain generation work +Fix flickering chunks on unload +Fix draw cell distance cache busting on far distances +Fix skybox not updating position with player entity +Add Scene shorthand for registering runnables as behavior trees diff --git a/src/main/java/electrosphere/client/terrain/cells/ClientDrawCellManager.java b/src/main/java/electrosphere/client/terrain/cells/ClientDrawCellManager.java index 6187e5c7..e2bc2079 100644 --- a/src/main/java/electrosphere/client/terrain/cells/ClientDrawCellManager.java +++ b/src/main/java/electrosphere/client/terrain/cells/ClientDrawCellManager.java @@ -209,7 +209,8 @@ public class ClientDrawCellManager { Globals.profiler.beginCpuSample("ClientDrawCellManager.split"); //perform op WorldOctTreeNode container = chunkTree.split(node); - container.setData(DrawCell.generateTerrainCell(container.getMinBound(), this.chunkTree.getMaxLevel() - container.getLevel())); + DrawCell containerCell = DrawCell.generateTerrainCell(container.getMinBound(), this.chunkTree.getMaxLevel() - container.getLevel()); + container.setData(containerCell); container.getData().transferChunkData(node.getData()); //do creations @@ -220,6 +221,7 @@ public class ClientDrawCellManager { child.getMinBound().z ); DrawCell drawCell = DrawCell.generateTerrainCell(cellWorldPos,this.chunkTree.getMaxLevel() - child.getLevel()); + drawCell.registerNotificationTarget(node.getData()); child.setLeaf(true); child.setData(drawCell); evaluationMap.put(child,true); @@ -233,13 +235,13 @@ public class ClientDrawCellManager { Globals.profiler.endCpuSample(); updated = true; - } else if(shouldRequest(playerPos, node, minLeafLod, distCache)){ + } else if(this.shouldRequest(playerPos, node, minLeafLod, distCache)){ Globals.profiler.beginCpuSample("ClientDrawCellManager.request"); //calculate what to request DrawCell cell = node.getData(); List highResFaces = null; - if(shouldSolveFaces(node,playerPos, distCache)){ + if(this.shouldSolveFaces(node,playerPos, distCache)){ highResFaces = this.solveHighResFace(node); } @@ -251,17 +253,17 @@ public class ClientDrawCellManager { Globals.profiler.endCpuSample(); updated = true; - } else if(shouldGenerate(playerPos, node, minLeafLod, distCache)){ + } else if(this.shouldGenerate(playerPos, node, minLeafLod, distCache)){ Globals.profiler.beginCpuSample("ClientDrawCellManager.generate"); int lodLevel = this.getLODLevel(node); //high res faces List highResFaces = null; - if(shouldSolveFaces(node,playerPos, distCache)){ + if(this.shouldSolveFaces(node,playerPos, distCache)){ highResFaces = this.solveHighResFace(node); } - if(containsDataToGenerate(node,highResFaces)){ + if(this.containsDataToGenerate(node,highResFaces)){ node.getData().generateDrawableEntity(textureAtlas, lodLevel, highResFaces); if(node.getData().getFailedGenerationAttempts() > FAILED_GENERATION_ATTEMPT_THRESHOLD){ node.getData().setHasRequested(false); @@ -284,21 +286,28 @@ public class ClientDrawCellManager { 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 = recursivelyUpdateCells(child, playerPos, evaluationMap, minLeafLod, distCache); + boolean childUpdate = this.recursivelyUpdateCells(child, playerPos, evaluationMap, minLeafLod, distCache); if(childUpdate == true){ updated = true; } - if(!child.getData().hasGenerated() || !child.getData().isHomogenous()){ + if(!child.getData().hasGenerated()){ + fullyGenerated = false; + } + if(!child.getData().isHomogenous()){ isHomogenous = false; } } + WorldOctTreeNode newNode = null; if(isHomogenous){ - WorldOctTreeNode newNode = this.join(node); - newNode.getData().setHasGenerated(true); + 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); } @@ -331,12 +340,17 @@ public class ClientDrawCellManager { if( lastPlayerPos.x / 16 != currentPlayerPos.x / 16 || lastPlayerPos.z / 16 != currentPlayerPos.z / 16 || lastPlayerPos.z / 16 != currentPlayerPos.z / 16 ){ - return SIXTEENTH_RES_LOD; + 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 EIGHTH_RES_LOD; + return SIXTEENTH_RES_LOD + 1; } if( lastPlayerPos.x / 4 != currentPlayerPos.x / 4 || lastPlayerPos.z / 4 != currentPlayerPos.z / 4 || lastPlayerPos.z / 4 != currentPlayerPos.z / 4 @@ -593,12 +607,14 @@ public class ClientDrawCellManager { */ private WorldOctTreeNode join(WorldOctTreeNode node){ Globals.profiler.beginCpuSample("ClientDrawCellManager.join"); - //perform op - WorldOctTreeNode newLeaf = chunkTree.join(node, DrawCell.generateTerrainCell(node.getMinBound(),node.getData().lod)); - newLeaf.getData().transferChunkData(node.getData()); - //do deletions - this.twoLayerDestroy(node); + //queue destructions prior to join -- the join operator clears all children on node + this.recursivelyDestroy(node); + + //perform op + DrawCell newLeafCell = DrawCell.generateTerrainCell(node.getMinBound(),node.getData().lod); + WorldOctTreeNode newLeaf = chunkTree.join(node, newLeafCell); + newLeaf.getData().transferChunkData(node.getData()); //update neighbors this.conditionalUpdateAdjacentNodes(newLeaf, newLeaf.getLevel()); @@ -663,7 +679,6 @@ public class ClientDrawCellManager { */ public boolean shouldGenerate(Vector3i pos, WorldOctTreeNode node, int minLeafLod, int distCache){ return - node.getData() != null && !node.getData().hasGenerated() && (this.chunkTree.getMaxLevel() - node.getLevel()) <= minLeafLod && ( @@ -725,7 +740,10 @@ public class ClientDrawCellManager { */ private void recursivelyDestroy(WorldOctTreeNode node){ if(node.getChildren().size() > 0){ - node.getChildren().forEach(child -> recursivelyDestroy(child)); + for(WorldOctTreeNode child : node.getChildren()){ + child.getData().destroy(); + } + // node.getChildren().forEach(child -> recursivelyDestroy(child)); } if(node.getData() != null){ node.getData().destroy(); diff --git a/src/main/java/electrosphere/client/terrain/cells/DrawCell.java b/src/main/java/electrosphere/client/terrain/cells/DrawCell.java index d248dc21..c22f3183 100644 --- a/src/main/java/electrosphere/client/terrain/cells/DrawCell.java +++ b/src/main/java/electrosphere/client/terrain/cells/DrawCell.java @@ -7,7 +7,6 @@ import org.joml.Vector3d; import org.joml.Vector3i; import electrosphere.client.terrain.cache.ChunkData; -import electrosphere.collision.CollisionEngine; import electrosphere.engine.Globals; import electrosphere.entity.ClientEntityUtils; import electrosphere.entity.Entity; @@ -88,6 +87,16 @@ public class DrawCell { * The cached minimum distance */ double cachedMinDistance = -1; + + /** + * Target to notify on generation completion + */ + DrawCell notifyTarget = null; + + /** + * The number of cells that have alerted this one + */ + int generationAlertCount = 0; /** @@ -161,10 +170,14 @@ public class DrawCell { } } } - if(modelEntity != null){ - ClientEntityUtils.destroyEntity(modelEntity); - } - modelEntity = TerrainChunk.clientCreateTerrainChunkEntity(this.chunkData, lod, atlas, this.hasPolygons()); + modelEntity = TerrainChunk.clientCreateTerrainChunkEntity( + this.chunkData, + this.notifyTarget, + this.modelEntity, + lod, + atlas, + this.hasPolygons() + ); ClientEntityUtils.initiallyPositionEntity(modelEntity, this.getRealPos(), new Quaterniond()); this.setHasGenerated(true); } @@ -188,6 +201,24 @@ public class DrawCell { 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(DrawCell 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 > 8){ + this.destroy(); + } + } /** * Destroys a drawcell including its physics @@ -200,8 +231,6 @@ public class DrawCell { if(framesSimulated < FRAMES_TO_WAIT_BEFORE_DESTRUCTION){ framesSimulated++; } else { - CollisionEngine collisionEngine = Globals.clientSceneWrapper.getCollisionEngine(); - collisionEngine.destroyPhysics(modelEntity); ClientEntityUtils.destroyEntity(modelEntity); Globals.clientScene.deregisterBehaviorTree(this); } diff --git a/src/main/java/electrosphere/client/terrain/manager/ClientTerrainManager.java b/src/main/java/electrosphere/client/terrain/manager/ClientTerrainManager.java index 31e60645..081dbf50 100644 --- a/src/main/java/electrosphere/client/terrain/manager/ClientTerrainManager.java +++ b/src/main/java/electrosphere/client/terrain/manager/ClientTerrainManager.java @@ -17,8 +17,11 @@ import electrosphere.client.scene.ClientWorldData; import electrosphere.client.terrain.cache.ChunkData; import electrosphere.client.terrain.cache.ClientTerrainCache; import electrosphere.client.terrain.cells.ClientDrawCellManager; +import electrosphere.client.terrain.cells.DrawCell; import electrosphere.client.terrain.cells.VoxelTextureAtlas; import electrosphere.engine.Globals; +import electrosphere.entity.ClientEntityUtils; +import electrosphere.entity.Entity; import electrosphere.entity.types.terrain.TerrainChunkData; import electrosphere.logger.LoggerInterface; import electrosphere.net.parser.net.message.TerrainMessage; @@ -277,11 +280,11 @@ public class ClientTerrainManager { * @param data The chunk data (triangles, normals, etc) * @return The model path that is promised to eventually reflect the terrain model when it makes it to gpu */ - public static String queueTerrainGridGeneration(TerrainChunkData data, VoxelTextureAtlas atlas){ + public static String queueTerrainGridGeneration(TerrainChunkData data, VoxelTextureAtlas atlas, DrawCell notifyTarget, Entity toDelete){ String promisedHash = ""; UUID newUUID = UUID.randomUUID(); promisedHash = newUUID.toString(); - TerrainChunkGenQueueItem queueItem = new TerrainChunkGenQueueItem(data, promisedHash, atlas); + TerrainChunkGenQueueItem queueItem = new TerrainChunkGenQueueItem(data, promisedHash, atlas, notifyTarget, toDelete); lock.acquireUninterruptibly(); terrainChunkGenerationQueue.add(queueItem); lock.release(); @@ -297,6 +300,12 @@ public class ClientTerrainManager { for(TerrainChunkGenQueueItem queueItem : terrainChunkGenerationQueue){ Model terrainModel = TerrainChunkModelGeneration.generateTerrainModel(queueItem.getData(), queueItem.getAtlas()); Globals.assetManager.registerModelToSpecificString(terrainModel, queueItem.getPromisedHash()); + if(queueItem.notifyTarget != null){ + queueItem.notifyTarget.alertToGeneration(); + } + if(queueItem.toDelete != null){ + ClientEntityUtils.destroyEntity(queueItem.toDelete); + } } terrainChunkGenerationQueue.clear(); lock.release(); diff --git a/src/main/java/electrosphere/client/terrain/manager/TerrainChunkGenQueueItem.java b/src/main/java/electrosphere/client/terrain/manager/TerrainChunkGenQueueItem.java index 07baebb3..ac87b234 100644 --- a/src/main/java/electrosphere/client/terrain/manager/TerrainChunkGenQueueItem.java +++ b/src/main/java/electrosphere/client/terrain/manager/TerrainChunkGenQueueItem.java @@ -1,6 +1,8 @@ package electrosphere.client.terrain.manager; +import electrosphere.client.terrain.cells.DrawCell; import electrosphere.client.terrain.cells.VoxelTextureAtlas; +import electrosphere.entity.Entity; import electrosphere.entity.types.terrain.TerrainChunkData; /** @@ -15,16 +17,35 @@ public class TerrainChunkGenQueueItem { //the texture atlas VoxelTextureAtlas atlas; + /** + * The draw cell to notify once this model is fully available to render + */ + DrawCell notifyTarget; + + /** + * The optional entity to delete on generation of this target + */ + Entity toDelete; + /** * Creates a queue item * @param data * @param promisedHash * @param atlas + * @param notifyTarget The draw cell to notify once this model is fully available to render */ - public TerrainChunkGenQueueItem(TerrainChunkData data, String promisedHash, VoxelTextureAtlas atlas){ + public TerrainChunkGenQueueItem( + TerrainChunkData data, + String promisedHash, + VoxelTextureAtlas atlas, + DrawCell notifyTarget, + Entity toDelete + ){ this.data = data; this.promisedHash = promisedHash; this.atlas = atlas; + this.notifyTarget = notifyTarget; + this.toDelete = toDelete; } /** diff --git a/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java b/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java index 93c50375..bc998374 100644 --- a/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java +++ b/src/main/java/electrosphere/engine/loadingthreads/ClientLoading.java @@ -250,6 +250,9 @@ public class ClientLoading { DrawableUtils.disableCulling(skybox); EntityUtils.getRotation(skybox).rotateX((float)(-Math.PI/2.0f)); EntityUtils.getScale(skybox).mul(600000.0f); + Globals.clientScene.registerBehaviorTree(() -> { + EntityUtils.getPosition(skybox).set(EntityUtils.getPosition(Globals.playerEntity)); + }); Globals.assetManager.queueOverrideMeshShader("Models/environment/skyboxSphere.fbx", "Sphere", "Shaders/entities/skysphere/skysphere.vs", "Shaders/entities/skysphere/skysphere.fs"); //cloud ring pseudo skybox diff --git a/src/main/java/electrosphere/entity/EntityDataStrings.java b/src/main/java/electrosphere/entity/EntityDataStrings.java index 74644788..0681a2dc 100644 --- a/src/main/java/electrosphere/entity/EntityDataStrings.java +++ b/src/main/java/electrosphere/entity/EntityDataStrings.java @@ -47,6 +47,7 @@ public class EntityDataStrings { Terrain Entity */ public static final String TERRAIN_IS_TERRAIN = "terrainEntity"; + public static final String CORRESPONDING_DRAW_CELL = "correspondingDrawCell"; /* * Fluid Entity diff --git a/src/main/java/electrosphere/entity/scene/Scene.java b/src/main/java/electrosphere/entity/scene/Scene.java index 686c6c86..eb3aba80 100644 --- a/src/main/java/electrosphere/entity/scene/Scene.java +++ b/src/main/java/electrosphere/entity/scene/Scene.java @@ -155,6 +155,16 @@ public class Scene { behaviorTreeList.add(tree); } + /** + * Registers a new behavior tree that executes a task + * @param task The task + */ + public void registerBehaviorTree(Runnable task){ + this.registerBehaviorTree(new BehaviorTree(){public void simulate(float deltaTime) { + task.run(); + }}); + } + /** * Deregisters a behavior tree from the scene * @param tree The behavior tree to deregister diff --git a/src/main/java/electrosphere/entity/types/terrain/TerrainChunk.java b/src/main/java/electrosphere/entity/types/terrain/TerrainChunk.java index 8839a2e2..2ad98d26 100644 --- a/src/main/java/electrosphere/entity/types/terrain/TerrainChunk.java +++ b/src/main/java/electrosphere/entity/types/terrain/TerrainChunk.java @@ -7,10 +7,12 @@ import org.joml.Quaterniond; import org.joml.Vector3d; import electrosphere.client.terrain.cells.ClientDrawCellManager; +import electrosphere.client.terrain.cells.DrawCell; import electrosphere.client.terrain.cells.VoxelTextureAtlas; import electrosphere.client.terrain.manager.ClientTerrainManager; import electrosphere.collision.PhysicsEntityUtils; import electrosphere.engine.Globals; +import electrosphere.entity.ClientEntityUtils; import electrosphere.entity.Entity; import electrosphere.entity.EntityCreationUtils; import electrosphere.entity.EntityDataStrings; @@ -36,11 +38,20 @@ public class TerrainChunk { /** * Creates a client terrain 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 terrain chunk entity */ - public static Entity clientCreateTerrainChunkEntity(TransvoxelChunkData chunkData, int levelOfDetail, VoxelTextureAtlas atlas, boolean hasPolygons){ + public static Entity clientCreateTerrainChunkEntity( + TransvoxelChunkData chunkData, + DrawCell notifyTarget, + Entity toDelete, + int levelOfDetail, + VoxelTextureAtlas atlas, + boolean hasPolygons + ){ Globals.profiler.beginAggregateCpuSample("TerrainChunk.clientCreateTerrainChunkEntity"); Entity rVal = EntityCreationUtils.createClientSpatialEntity(); @@ -52,7 +63,7 @@ public class TerrainChunk { data = TransvoxelModelGeneration.generateTerrainChunkData(chunkData); data.constructBuffers(); if(Globals.clientScene.containsEntity(rVal)){ - String modelPath = ClientTerrainManager.queueTerrainGridGeneration(data, atlas); + String modelPath = ClientTerrainManager.queueTerrainGridGeneration(data, atlas, notifyTarget, toDelete); EntityCreationUtils.makeEntityDrawablePreexistingModel(rVal, modelPath); if(levelOfDetail == ClientDrawCellManager.FULL_RES_LOD){ PhysicsEntityUtils.clientAttachTerrainChunkRigidBody(rVal, data); @@ -63,6 +74,12 @@ public class TerrainChunk { } rVal.putData(EntityDataStrings.HAS_UNIQUE_MODEL, true); } else { + if(notifyTarget != null){ + notifyTarget.alertToGeneration(); + } + if(toDelete != null){ + ClientEntityUtils.destroyEntity(toDelete); + } LoggerInterface.loggerEngine.DEBUG("Finished generating terrain polygons; however, entity has already been deleted."); } } catch (Error e){ @@ -71,6 +88,13 @@ public class TerrainChunk { LoggerInterface.loggerEngine.ERROR(e); } }); + } else { + if(notifyTarget != null){ + notifyTarget.alertToGeneration(); + } + if(toDelete != null){ + ClientEntityUtils.destroyEntity(toDelete); + } } rVal.putData(EntityDataStrings.TERRAIN_IS_TERRAIN, true);