Improve node out-of-bound logic
This fixes some overlapping rendering issues, fixes LOD generating outside of render distance, and fixes low-detail LODs flashing when moving into previously-explored LODs
This commit is contained in:
+85
-46
@@ -246,21 +246,37 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
|
||||
//region
|
||||
|
||||
// remove out of bound sections
|
||||
this.setCenterBlockPos(playerPos, (renderSection) ->
|
||||
{
|
||||
if (renderSection != null)
|
||||
this.setCenterBlockPos(playerPos,
|
||||
// remove completely out of bound nodes
|
||||
// (the root node is no longer in bounds)
|
||||
(renderSection) ->
|
||||
{
|
||||
this.fullDataSourceProvider.removeRetrievalRequestIf((long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
|
||||
|
||||
// unfortunately we have to fully go through each set
|
||||
// since a removed position may be larger than the multiple generated positions
|
||||
// it contains
|
||||
this.missingGenerationPosSet.removeIf((Long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
|
||||
this.queuedGenerationPosSet.removeIf((Long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
|
||||
|
||||
renderSection.close();
|
||||
if (renderSection != null)
|
||||
{
|
||||
this.fullDataSourceProvider.removeRetrievalRequestIf((long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
|
||||
|
||||
// unfortunately we have to fully go through each set
|
||||
// since a removed position may be larger than the multiple generated positions
|
||||
// it contains
|
||||
this.missingGenerationPosSet.removeIf((Long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
|
||||
this.queuedGenerationPosSet.removeIf((Long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
|
||||
|
||||
renderSection.close();
|
||||
}
|
||||
},
|
||||
// mutate partially out of bound nodes
|
||||
// (the root node is still in bounds, but this individual child node isn't)
|
||||
(renderSection) ->
|
||||
{
|
||||
if (renderSection != null)
|
||||
{
|
||||
// when this node comes back into render distance
|
||||
// we'll need to re-load it since the full data
|
||||
// may have been modified while it was out of bounds
|
||||
renderSection.renderDataDirty = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
//endregion
|
||||
|
||||
@@ -309,7 +325,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
|
||||
continue;
|
||||
}
|
||||
|
||||
node.value.retreivedMissingSectionsForRetreival = false;
|
||||
node.value.queuedMissingSectionsForRetrieval = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,9 +468,9 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
|
||||
|
||||
// since this section wants to render
|
||||
// check if it needs any generation to do so
|
||||
if (!node.value.retreivedMissingSectionsForRetreival)
|
||||
if (!node.value.queuedMissingSectionsForRetrieval)
|
||||
{
|
||||
node.value.retreivedMissingSectionsForRetreival = true;
|
||||
node.value.queuedMissingSectionsForRetrieval = true;
|
||||
this.tryQueuePosForRetrieval(node.value.pos); // can be quite slow
|
||||
}
|
||||
}
|
||||
@@ -481,27 +497,6 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
|
||||
//=========================//
|
||||
//region
|
||||
|
||||
@NotNull
|
||||
private QuadNode<LodRenderSection> tryAddNodeToTree(
|
||||
@NotNull QuadNode<LodRenderSection> rootNode,
|
||||
@Nullable QuadNode<LodRenderSection> quadNode,
|
||||
long sectionPos // section pos is needed here since the quad node may be null
|
||||
)
|
||||
{
|
||||
// create the node
|
||||
if (quadNode == null)
|
||||
{
|
||||
rootNode.setValue(sectionPos, new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider));
|
||||
quadNode = rootNode.getNode(sectionPos);
|
||||
}
|
||||
if (quadNode == null)
|
||||
{
|
||||
LodUtil.assertNotReach("Unable to add node with pos ["+DhSectionPos.toString(sectionPos)+"] to tree root ["+rootNode+"].");
|
||||
}
|
||||
|
||||
return quadNode;
|
||||
}
|
||||
|
||||
/** @return true if the node at this position has uploaded its render data */
|
||||
private boolean recursivelyUpdateRenderSectionNode(
|
||||
@NotNull DhBlockPos2D playerPos,
|
||||
@@ -520,13 +515,15 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
|
||||
quadNode = this.tryAddNodeToTree(rootNode, quadNode, sectionPos);
|
||||
|
||||
|
||||
//// Skip sections that are out-of-bounds.
|
||||
//// If not done some sections will appear and/or generate
|
||||
//// outside the desired render distance
|
||||
//if (!this.isSectionPosInBounds(quadNode.sectionPos))
|
||||
//{
|
||||
// return true;
|
||||
//}
|
||||
// Skip sections that are out-of-bounds.
|
||||
// If not done some sections will appear and/or generate
|
||||
// outside the desired render distance
|
||||
if (!this.isSectionPosInBounds(quadNode.sectionPos))
|
||||
{
|
||||
this.tickNodeHolder.addDisableNode(quadNode);
|
||||
this.recursivelyDisableChildNodes(quadNode);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// make sure the render section is created (shouldn't be necessary, but just in case)
|
||||
@@ -629,6 +626,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
|
||||
{
|
||||
// not all child positions are loaded yet, this one should be rendered instead
|
||||
this.tickNodeHolder.addEnableNode(quadNode);
|
||||
this.recursivelyDisableChildNodes(quadNode);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -639,6 +637,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
|
||||
return nodeCanRender;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return true if the node at this position has uploaded its render data */
|
||||
private boolean onDesiredDetailLevel(
|
||||
@NotNull QuadNode<LodRenderSection> quadNode,
|
||||
@@ -655,16 +654,56 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
|
||||
if (quadNode.value != null
|
||||
&& quadNode.value.gpuUploadComplete())
|
||||
{
|
||||
this.tickNodeHolder.addEnableDeleteChildrenNode(quadNode);
|
||||
return true;
|
||||
if (!this.tickNodeHolder.getEnabledNodes().contains(parentNode))
|
||||
this.tickNodeHolder.addEnableDeleteChildrenNode(quadNode);
|
||||
else
|
||||
this.tickNodeHolder.addDisableNode(quadNode);
|
||||
|
||||
return true; // TODO broken, will enable sections even if parent is enabled
|
||||
}
|
||||
else
|
||||
{
|
||||
this.tickNodeHolder.addDisableNode(quadNode);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@NotNull
|
||||
private QuadNode<LodRenderSection> tryAddNodeToTree(
|
||||
@NotNull QuadNode<LodRenderSection> rootNode,
|
||||
@Nullable QuadNode<LodRenderSection> quadNode,
|
||||
long sectionPos // section pos is needed here since the quad node may be null
|
||||
)
|
||||
{
|
||||
// create the node
|
||||
if (quadNode == null)
|
||||
{
|
||||
rootNode.setValue(sectionPos, new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider));
|
||||
quadNode = rootNode.getNode(sectionPos);
|
||||
}
|
||||
if (quadNode == null)
|
||||
{
|
||||
LodUtil.assertNotReach("Unable to add node with pos ["+DhSectionPos.toString(sectionPos)+"] to tree root ["+rootNode+"].");
|
||||
}
|
||||
|
||||
return quadNode;
|
||||
}
|
||||
|
||||
private void recursivelyDisableChildNodes(@NotNull QuadNode<LodRenderSection> quadNode)
|
||||
{
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
QuadNode<LodRenderSection> childNode = quadNode.getChildByIndex(i);
|
||||
this.tickNodeHolder.removeEnableAndDisableNode(childNode);
|
||||
|
||||
if (childNode != null)
|
||||
{
|
||||
this.recursivelyDisableChildNodes(childNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//endregion
|
||||
|
||||
//=====================//
|
||||
|
||||
+17
-8
@@ -34,11 +34,8 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
|
||||
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
|
||||
import com.seibel.distanthorizons.core.pos.DhSectionPos;
|
||||
import com.seibel.distanthorizons.core.render.renderer.AbstractDebugWireframeRenderer;
|
||||
import com.seibel.distanthorizons.core.render.renderer.BeaconRenderHandler;
|
||||
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
|
||||
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.LodBufferContainer;
|
||||
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
|
||||
import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo;
|
||||
import com.seibel.distanthorizons.core.util.LodUtil;
|
||||
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
|
||||
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
|
||||
@@ -48,10 +45,8 @@ import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.annotation.WillNotClose;
|
||||
import java.awt.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* A render section represents an area that could be rendered.
|
||||
@@ -75,8 +70,16 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
|
||||
|
||||
|
||||
private boolean renderingEnabled = false;
|
||||
private boolean beaconsRendering = false;
|
||||
public boolean retreivedMissingSectionsForRetreival = false;
|
||||
/**
|
||||
* Used when a node goes out of render distance
|
||||
* but isn't removed from the underlying quad tree structure. <br><br>
|
||||
*
|
||||
* In those cases we should act as if the node was removed
|
||||
* for cached render data caching purposes, but not
|
||||
* for re-creating missing nodes.
|
||||
*/
|
||||
public boolean renderDataDirty = false;
|
||||
public boolean queuedMissingSectionsForRetrieval = false;
|
||||
|
||||
/** this reference is necessary so we can determine what VBO to render */
|
||||
public LodBufferContainer renderBufferContainer;
|
||||
@@ -320,6 +323,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
|
||||
|
||||
// upload complete
|
||||
this.renderBufferContainer = buffer.buffersUploaded ? buffer : null;
|
||||
this.renderDataDirty = false;
|
||||
|
||||
if (previousContainer != null)
|
||||
{
|
||||
@@ -345,7 +349,12 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
|
||||
//=================//
|
||||
//region
|
||||
|
||||
public boolean gpuUploadComplete() { return this.renderBufferContainer != null; }
|
||||
/** aka "canRender()" */
|
||||
public boolean gpuUploadComplete()
|
||||
{
|
||||
return this.renderBufferContainer != null
|
||||
&& !this.renderDataDirty;
|
||||
}
|
||||
|
||||
public boolean getRenderingEnabled() { return this.renderingEnabled; }
|
||||
public void setRenderingEnabled(boolean enabled) { this.renderingEnabled = enabled;}
|
||||
|
||||
+27
-9
@@ -63,21 +63,39 @@ public class QuadTreeTickNodeHolder
|
||||
{
|
||||
if(this.presentNodes.add(node))
|
||||
{
|
||||
// not a big fan of having to check every node to prevent overlaps, but it does work
|
||||
this.nodesToEnable.removeIf((QuadNode<LodRenderSection> checkNode) ->
|
||||
// in James testing as of 4-21-2026
|
||||
// this should no longer be needed to prevent overlaps,
|
||||
// however I'm keeping it here as a quick fix solution if
|
||||
// the problem comes up again
|
||||
if (false)
|
||||
{
|
||||
boolean contained = DhSectionPos.contains(node.sectionPos, checkNode.sectionPos);
|
||||
if (contained)
|
||||
// not a big fan of having to check every node to prevent overlaps, but it does work
|
||||
this.nodesToEnable.removeIf((QuadNode<LodRenderSection> checkNode) ->
|
||||
{
|
||||
this.nodesToDisable.add(checkNode);
|
||||
}
|
||||
|
||||
return contained;
|
||||
});
|
||||
boolean contained = DhSectionPos.contains(node.sectionPos, checkNode.sectionPos);
|
||||
if (contained)
|
||||
{
|
||||
this.nodesToDisable.add(checkNode);
|
||||
}
|
||||
|
||||
return contained;
|
||||
});
|
||||
}
|
||||
|
||||
this.nodesToEnable.add(node);
|
||||
}
|
||||
}
|
||||
|
||||
/** */
|
||||
public void removeEnableAndDisableNode(QuadNode<LodRenderSection> node)
|
||||
{
|
||||
this.nodesToEnable.remove(node);
|
||||
this.nodesToEnableDeleteChildrenList.remove(node);
|
||||
|
||||
this.presentNodes.add(node); // should already be present, but re-added just in case
|
||||
this.nodesToDisable.add(node); // node shouldn't be rendered since it's being disabled by a parent
|
||||
}
|
||||
|
||||
public HashSet<QuadNode<LodRenderSection>> getEnabledNodes() { return this.nodesToEnable; }
|
||||
|
||||
|
||||
|
||||
+95
-20
@@ -19,10 +19,12 @@
|
||||
|
||||
package com.seibel.distanthorizons.core.util.objects.quadTree;
|
||||
|
||||
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
|
||||
import com.seibel.distanthorizons.core.logging.DhLogger;
|
||||
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
|
||||
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
|
||||
import com.seibel.distanthorizons.core.pos.DhSectionPos;
|
||||
import com.seibel.distanthorizons.core.render.QuadTree.LodQuadTree;
|
||||
import com.seibel.distanthorizons.core.util.LodUtil;
|
||||
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
|
||||
import com.seibel.distanthorizons.coreapi.util.MathUtil;
|
||||
@@ -61,6 +63,11 @@ public class QuadTree<T>
|
||||
private final MovableGridRingList<QuadNode<T>> topRingList;
|
||||
|
||||
private DhBlockPos2D centerBlockPos;
|
||||
/**
|
||||
* defines how many blocks the center needs to move in blocks
|
||||
* before we check for out-of-bound nodes.
|
||||
*/
|
||||
private int blockDistanceForNodeClearing = FullDataSourceV2.WIDTH;
|
||||
|
||||
|
||||
|
||||
@@ -354,35 +361,103 @@ public class QuadTree<T>
|
||||
//================//
|
||||
//region
|
||||
|
||||
public void setCenterBlockPos(DhBlockPos2D newCenterPos) { this.setCenterBlockPos(newCenterPos, null); }
|
||||
public void setCenterBlockPos(DhBlockPos2D newCenterPos, @Nullable Consumer<? super T> removedItemConsumer)
|
||||
public void setCenterBlockPos(DhBlockPos2D newCenterPos) { this.setCenterBlockPos(newCenterPos, null, null); }
|
||||
/**
|
||||
* @param removedConsumer fired when a root node is completely removed from the underlying data structure
|
||||
* @param mutateOutOfBoundConsumer fired when a child node is out of bounds, but not removed from the underlying data structure
|
||||
*/
|
||||
public void setCenterBlockPos(
|
||||
DhBlockPos2D newCenterPos,
|
||||
@Nullable Consumer<? super T> removedConsumer,
|
||||
@Nullable Consumer<? super T> mutateOutOfBoundConsumer)
|
||||
{
|
||||
this.centerBlockPos = newCenterPos;
|
||||
|
||||
MovableGridRingList.Pos2D expectedCenterPos = new MovableGridRingList.Pos2D(
|
||||
BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, this.treeRootDetailLevel),
|
||||
BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, this.treeRootDetailLevel));
|
||||
|
||||
if (this.topRingList.getCenter().equals(expectedCenterPos))
|
||||
// did we move significantly?
|
||||
boolean ringListMoved = false;
|
||||
int newCenterPosX = BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, this.treeRootDetailLevel);
|
||||
int newCenterPosZ = BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, this.treeRootDetailLevel);
|
||||
if (this.topRingList.getCenter().getX() == newCenterPosX
|
||||
&& this.topRingList.getCenter().getY() == newCenterPosZ)
|
||||
{
|
||||
// tree doesn't need to be moved
|
||||
ringListMoved = true;
|
||||
}
|
||||
|
||||
// did we move a little bit?
|
||||
boolean recalculateOutOfBoundNodes = false;
|
||||
int centerBlockDistance = this.centerBlockPos.manhattanDist(newCenterPos);
|
||||
if (centerBlockDistance < this.blockDistanceForNodeClearing)
|
||||
{
|
||||
recalculateOutOfBoundNodes = true;
|
||||
}
|
||||
|
||||
if (!ringListMoved
|
||||
&& !recalculateOutOfBoundNodes)
|
||||
{
|
||||
// the tree didn't move enough that we need
|
||||
// to re-calculate anything
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// remove out of bounds root nodes
|
||||
this.topRingList.moveTo(expectedCenterPos.getX(), expectedCenterPos.getY(), (quadNode) ->
|
||||
|
||||
this.centerBlockPos = newCenterPos;
|
||||
|
||||
// remove out of bound root nodes
|
||||
this.topRingList.moveTo(newCenterPosX, newCenterPosZ, (quadNode) ->
|
||||
{
|
||||
if (quadNode != null)
|
||||
{
|
||||
quadNode.deleteAllChildren(removedItemConsumer);
|
||||
quadNode.deleteAllChildren(removedConsumer);
|
||||
|
||||
if (removedItemConsumer != null)
|
||||
if (removedConsumer != null)
|
||||
{
|
||||
removedItemConsumer.accept(quadNode.value);
|
||||
removedConsumer.accept(quadNode.value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// mutate out of bound child nodes
|
||||
this.topRingList.forEach((rootNode) ->
|
||||
{
|
||||
this.mutateOutOfBoundChildNodes(rootNode, mutateOutOfBoundConsumer);
|
||||
});
|
||||
}
|
||||
/**
|
||||
* we don't want to actually remove nodes or node data
|
||||
* since that can cause the {@link LodQuadTree} to
|
||||
* flash low-detail LODs when moving into previously-loaded
|
||||
* LODs, which is really disorienting.
|
||||
*/
|
||||
private void mutateOutOfBoundChildNodes(@Nullable QuadNode<T> quadNode, @Nullable Consumer<? super T> mutateOutOfBoundConsumer)
|
||||
{
|
||||
// nodes shouldn't be null, but just in case
|
||||
if (quadNode == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// go over each child node
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
QuadNode<T> childNode = quadNode.getChildByIndex(i);
|
||||
if (childNode == null
|
||||
|| childNode.value == null)
|
||||
{
|
||||
// no need to go any deeper if this node is already empty
|
||||
continue;
|
||||
}
|
||||
|
||||
// mutate nodes from the bottom up
|
||||
this.mutateOutOfBoundChildNodes(childNode, mutateOutOfBoundConsumer);
|
||||
|
||||
// mutate this node if out of bounds
|
||||
if (!this.isSectionPosInBounds(childNode.sectionPos))
|
||||
{
|
||||
if (mutateOutOfBoundConsumer != null)
|
||||
{
|
||||
mutateOutOfBoundConsumer.accept(childNode.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public final DhBlockPos2D getCenterBlockPos() { return this.centerBlockPos; }
|
||||
@@ -487,7 +562,7 @@ public class QuadTree<T>
|
||||
|
||||
private class QuadTreeNodeIterator implements Iterator<QuadNode<T>>
|
||||
{
|
||||
private final QuadTreeRootPosIterator rootNodeIterator;
|
||||
private final QuadTreeRootPosIterator rootNodePosIterator;
|
||||
private Iterator<QuadNode<T>> currentNodeIterator;
|
||||
|
||||
private QuadNode<T> lastNode = null;
|
||||
@@ -500,7 +575,7 @@ public class QuadTree<T>
|
||||
|
||||
public QuadTreeNodeIterator(boolean onlyReturnLeaves, @Nullable INodeIteratorStoppingFunc<T> stopIteratingFunc)
|
||||
{
|
||||
this.rootNodeIterator = new QuadTreeRootPosIterator(false, stopIteratingFunc);
|
||||
this.rootNodePosIterator = new QuadTreeRootPosIterator(false, stopIteratingFunc);
|
||||
this.onlyReturnLeaves = onlyReturnLeaves;
|
||||
|
||||
this.stopIteratingFunc = stopIteratingFunc;
|
||||
@@ -511,7 +586,7 @@ public class QuadTree<T>
|
||||
@Override
|
||||
public boolean hasNext()
|
||||
{
|
||||
if (!this.rootNodeIterator.hasNext()
|
||||
if (!this.rootNodePosIterator.hasNext()
|
||||
&& this.currentNodeIterator != null
|
||||
&& !this.currentNodeIterator.hasNext())
|
||||
{
|
||||
@@ -547,9 +622,9 @@ public class QuadTree<T>
|
||||
{
|
||||
Iterator<QuadNode<T>> nodeIterator = null;
|
||||
while ((nodeIterator == null || !nodeIterator.hasNext())
|
||||
&& this.rootNodeIterator.hasNext())
|
||||
&& this.rootNodePosIterator.hasNext())
|
||||
{
|
||||
long sectionPos = this.rootNodeIterator.nextLong();
|
||||
long sectionPos = this.rootNodePosIterator.nextLong();
|
||||
|
||||
// try-get to prevent concurrency errors if the tree is being moved while we walk through it
|
||||
QuadNode<T> rootNode = QuadTree.this.tryGetNode(sectionPos);
|
||||
|
||||
Reference in New Issue
Block a user