From d33be490a7766ade8bf9150c66fab2d3856ebfac Mon Sep 17 00:00:00 2001 From: James Seibel Date: Mon, 29 Sep 2025 07:28:03 -0500 Subject: [PATCH] cull LOD rendering on the quad tree --- .../core/render/LodQuadTree.java | 4 +- .../core/render/RenderBufferHandler.java | 90 +++++----- .../core/util/objects/quadTree/QuadNode.java | 30 ++-- .../core/util/objects/quadTree/QuadTree.java | 103 +++++++---- .../iterators/QuadNodeChildIndexIterator.java | 2 +- .../iterators/QuadTreeNodeIterator.java | 52 ++++-- core/src/test/java/tests/QuadTreeTest.java | 170 +++++++++++++++--- 7 files changed, 321 insertions(+), 130 deletions(-) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/LodQuadTree.java b/core/src/main/java/com/seibel/distanthorizons/core/render/LodQuadTree.java index 05224c165..8083602fc 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/LodQuadTree.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/LodQuadTree.java @@ -342,7 +342,7 @@ public class LodQuadTree extends QuadTree implements IDebugRen // outdated when child LODs are updated. // (They'd have to be reloaded from file anyway during an update) long parentPos = renderSection.pos; - while (DhSectionPos.getDetailLevel(parentPos) <= this.treeMinDetailLevel) + while (DhSectionPos.getDetailLevel(parentPos) <= this.treeRootDetailLevel) { QuadNode parentNode = this.getNode(parentPos); if (parentNode != null) @@ -579,7 +579,7 @@ public class LodQuadTree extends QuadTree implements IDebugRen // If not done corners may not be flush with the other LODs, which looks bad. byte minSectionDetailLevel = this.getDetailLevelFromDistance(this.blockRenderDistanceDiameter); // get the minimum allowed detail level minSectionDetailLevel -= 1; // -1 so corners can't render lower than their adjacent neighbors. space - minSectionDetailLevel = (byte) Math.min(minSectionDetailLevel, this.treeMinDetailLevel); // don't allow rendering lower detail sections than what the tree contains + minSectionDetailLevel = (byte) Math.min(minSectionDetailLevel, this.treeRootDetailLevel); // don't allow rendering lower detail sections than what the tree contains this.minRenderDetailLevel = (byte) Math.max(minSectionDetailLevel, this.maxRenderDetailLevel); // respect the user's selected max resolution if it is lower detail (IE they want 2x2 block, but minSectionDetailLevel is specifically for 1x1 block render resolution) } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/RenderBufferHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/render/RenderBufferHandler.java index a39eab027..ad0aa7956 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/RenderBufferHandler.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/RenderBufferHandler.java @@ -35,6 +35,7 @@ import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.Pos2D; import com.seibel.distanthorizons.core.render.renderer.LodRenderer; import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.core.util.objects.RollingAverage; import com.seibel.distanthorizons.core.util.objects.SortedArraySet; import com.seibel.distanthorizons.core.util.objects.quadTree.QuadNode; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftGLWrapper; @@ -174,50 +175,17 @@ public class RenderBufferHandler implements AutoCloseable } } - Pos2D cPos = this.lodQuadTree.getCenterBlockPos().toPos2D(); + Pos2D centerPos = this.lodQuadTree.getCenterBlockPos().toPos2D(); // Now that we have the axis directions, we can sort the render list Comparator farToNearComparator = (loadedBufferA, loadedBufferB) -> { Pos2D aPos = DhSectionPos.getCenterBlockPos(loadedBufferA.pos).toPos2D(); Pos2D bPos = DhSectionPos.getCenterBlockPos(loadedBufferB.pos).toPos2D(); - if (true) - { - int aManhattanDistance = aPos.manhattanDist(cPos); - int bManhattanDistance = bPos.manhattanDist(cPos); - return bManhattanDistance - aManhattanDistance; - } - for (EDhDirection axisDirection : axisDirections) - { - if (axisDirection.getAxis().isVertical()) - { - continue; // We only sort in the horizontal direction - } - - int abPosDifference; - if (axisDirection.getAxis().equals(EDhDirection.Axis.X)) - { - abPosDifference = aPos.getX() - bPos.getX(); - } - else - { - abPosDifference = aPos.getY() - bPos.getY(); - } - - if (abPosDifference == 0) - { - continue; - } - - if (axisDirection.getAxisDirection().equals(EDhDirection.AxisDirection.NEGATIVE)) - { - abPosDifference = -abPosDifference; // Reverse the sign - } - return abPosDifference; - } - - return DhSectionPos.getDetailLevel(loadedBufferA.pos) - DhSectionPos.getDetailLevel(loadedBufferB.pos); // If all else fails, sort by detail + int aManhattanDistance = aPos.manhattanDist(centerPos); + int bManhattanDistance = bPos.manhattanDist(centerPos); + return bManhattanDistance - aManhattanDistance; }; this.loadedNearToFarBuffers = new SortedArraySet<>((a, b) -> -farToNearComparator.compare(a, b)); // TODO is the comparator named wrong? @@ -277,19 +245,21 @@ public class RenderBufferHandler implements AutoCloseable this.culledBufferCount = 0; } - boolean rebuildAllBuffers = this.rebuildAllBuffers.getAndSet(false); - Iterator> nodeIterator = this.lodQuadTree.nodeIterator(); - while (nodeIterator.hasNext()) + // setup iterator with culling frustum + Iterator> nodeIterator = this.lodQuadTree.nodeIteratorWithStoppingFilter((QuadNode node) -> { - QuadNode node = nodeIterator.next(); + if (node == null) + { + return true; + } - long sectionPos = node.sectionPos; LodRenderSection renderSection = node.value; if (renderSection == null) { - continue; + return false; } + try { if (enableFrustumCulling) @@ -309,22 +279,48 @@ public class RenderBufferHandler implements AutoCloseable this.culledBufferCount++; } - continue; + return true; } } + return false; + } + catch (Exception e) + { + LOGGER.error("Unexpected issue during culling for node pos: ["+DhSectionPos.toString(node.sectionPos)+"], error: ["+e.getMessage()+"].", e); + + // don't cull if there was an unexpected issue + return false; + } + }); + + while (nodeIterator.hasNext()) + { + QuadNode node = nodeIterator.next(); + + long sectionPos = node.sectionPos; + LodRenderSection renderSection = node.value; + if (renderSection == null) + { + continue; + } + + + + try + { ColumnRenderBuffer buffer = renderSection.renderBuffer; - if (buffer == null || !renderSection.getRenderingEnabled()) + if (buffer == null + || !renderSection.getRenderingEnabled()) { continue; } - this.loadedNearToFarBuffers.add(new LoadedRenderBuffer(buffer, sectionPos)); } catch (Exception e) { - LOGGER.error("Error updating QuadTree render source at " + renderSection.pos + ".", e); + LOGGER.error("Error updating QuadTree render source at [" + DhSectionPos.toString(renderSection.pos) + "], error: ["+e.getMessage()+"].", e); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadNode.java b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadNode.java index 86e5ea680..3305a02a1 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadNode.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadNode.java @@ -27,6 +27,7 @@ import com.seibel.distanthorizons.core.util.objects.quadTree.iterators.QuadNodeD import com.seibel.distanthorizons.core.util.objects.quadTree.iterators.QuadTreeNodeIterator; import it.unimi.dsi.fastutil.longs.LongIterator; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; import java.util.Iterator; import java.util.function.Consumer; @@ -37,7 +38,11 @@ public class QuadNode public final long sectionPos; - public final byte minimumDetailLevel; + /** + * this is the highest detail level this tree can provide. + * IE the detail levels that the root nodes in the tree are. + */ + public final byte parentTreeLeafDetailLevel; public T value; @@ -68,10 +73,10 @@ public class QuadNode - public QuadNode(long sectionPos, byte minimumDetailLevel) + public QuadNode(long sectionPos, byte parentTreeLeafDetailLevel) { this.sectionPos = sectionPos; - this.minimumDetailLevel = minimumDetailLevel; + this.parentTreeLeafDetailLevel = parentTreeLeafDetailLevel; } @@ -191,12 +196,12 @@ public class QuadNode if (DhSectionPos.getDetailLevel(inputSectionPos) == DhSectionPos.getDetailLevel(this.sectionPos) && inputSectionPos != this.sectionPos) { - throw new IllegalArgumentException("Node and input detail level are equal, however positions are not; this tree doesn't contain the requested position. Node pos: " + this.sectionPos + ", input pos: " + inputSectionPos); + throw new IllegalArgumentException("Node and input detail level are equal, however positions are not; this tree doesn't contain the requested position. Node pos: " + this.sectionPos + ", input pos: " + DhSectionPos.toString(inputSectionPos)); } - if (DhSectionPos.getDetailLevel(inputSectionPos) < this.minimumDetailLevel) + if (DhSectionPos.getDetailLevel(inputSectionPos) < this.parentTreeLeafDetailLevel) { - throw new IllegalArgumentException("Input position is requesting a detail level lower than what this node can provide. Node minimum detail level: " + this.minimumDetailLevel + ", input pos: " + inputSectionPos); + throw new IllegalArgumentException("Input position is requesting a detail level lower than what this node can provide. Tree leaf detail level: " + this.parentTreeLeafDetailLevel + ", input pos: " + DhSectionPos.toString(inputSectionPos)); } @@ -231,7 +236,7 @@ public class QuadNode if (replaceValue && this.nwChild == null) { // if no node exists for this position, but we want to insert a new value at this position, create a new node - this.nwChild = new QuadNode<>(nwPos, this.minimumDetailLevel); + this.nwChild = new QuadNode<>(nwPos, this.parentTreeLeafDetailLevel); } childNode = this.nwChild; @@ -244,7 +249,7 @@ public class QuadNode if (replaceValue && this.swChild == null) { // if no node exists for this position, but we want to insert a new value at this position, create a new node - this.swChild = new QuadNode<>(swPos, this.minimumDetailLevel); + this.swChild = new QuadNode<>(swPos, this.parentTreeLeafDetailLevel); } childNode = this.swChild; @@ -257,7 +262,7 @@ public class QuadNode if (replaceValue && this.neChild == null) { // if no node exists for this position, but we want to insert a new value at this position, create a new node - this.neChild = new QuadNode<>(nePos, this.minimumDetailLevel); + this.neChild = new QuadNode<>(nePos, this.parentTreeLeafDetailLevel); } childNode = this.neChild; @@ -270,7 +275,7 @@ public class QuadNode if (replaceValue && this.seChild == null) { // if no node exists for this position, but we want to insert a new value at this position, create a new node - this.seChild = new QuadNode<>(sePos, this.minimumDetailLevel); + this.seChild = new QuadNode<>(sePos, this.parentTreeLeafDetailLevel); } childNode = this.seChild; @@ -290,8 +295,9 @@ public class QuadNode // iterators // //===========// - public Iterator> getNodeIterator() { return new QuadTreeNodeIterator<>(this, false); } - public Iterator> getLeafNodeIterator() { return new QuadTreeNodeIterator<>(this, true); } + public Iterator> getNodeIterator() { return new QuadTreeNodeIterator<>(this, false, null); } + public Iterator> getNodeIterator(@Nullable QuadTree.INodeIteratorStoppingFunc stopIteratingFunc) { return new QuadTreeNodeIterator<>(this, false, stopIteratingFunc); } + public Iterator> getLeafNodeIterator() { return new QuadTreeNodeIterator<>(this, true, null); } /** positions can point to null children */ public LongIterator getChildPosIterator() { return new QuadNodeDirectChildPosIterator<>(this); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadTree.java b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadTree.java index 54a7aaafc..175c0fca4 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadTree.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadTree.java @@ -35,6 +35,7 @@ import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.LongConsumer; /** @@ -50,12 +51,12 @@ public class QuadTree * The largest numerical detail level this tree supports.
* IE: the detail level used by the root nodes. */ - public final byte treeMinDetailLevel; + public final byte treeRootDetailLevel; /** * The smallest numerical detail level this tree supports.
* IE: the detail level used by the leaf nodes. */ - public final byte treeMaxDetailLevel; + public final byte treeLeafDetailLevel; private final int diameterInBlocks; // diameterInBlocks @@ -71,21 +72,21 @@ public class QuadTree * * @param diameterInBlocks equivalent to the distance between the two opposing sides */ - public QuadTree(int diameterInBlocks, DhBlockPos2D centerBlockPos, byte treeMaxDetailLevel) + public QuadTree(int diameterInBlocks, DhBlockPos2D centerBlockPos, byte treeLeafDetailLevel) { this.centerBlockPos = centerBlockPos; this.diameterInBlocks = diameterInBlocks; - this.treeMaxDetailLevel = treeMaxDetailLevel; + this.treeLeafDetailLevel = treeLeafDetailLevel; // the min detail level must be greater than 0 (to prevent divide by 0 errors) and greater than the maximum detail level - this.treeMinDetailLevel = (byte) Math.max(Math.max(1, this.treeMaxDetailLevel), MathUtil.log2(diameterInBlocks)); + this.treeRootDetailLevel = (byte) Math.max(Math.max(1, this.treeLeafDetailLevel), MathUtil.log2(diameterInBlocks)); - int halfSizeInRootNodes = Math.floorDiv(this.diameterInBlocks, 2) / BitShiftUtil.powerOfTwo(this.treeMinDetailLevel); + int halfSizeInRootNodes = Math.floorDiv(this.diameterInBlocks, 2) / BitShiftUtil.powerOfTwo(this.treeRootDetailLevel); halfSizeInRootNodes = halfSizeInRootNodes + 1; // always add 1 so nodes will always have a parent, even if the tree's center is offset from the root node grid Pos2D ringListCenterPos = new Pos2D( - BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, this.treeMinDetailLevel), - BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, this.treeMinDetailLevel)); + BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, this.treeRootDetailLevel), + BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, this.treeRootDetailLevel)); this.topRingList = new MovableGridRingList<>(halfSizeInRootNodes, ringListCenterPos.getX(), ringListCenterPos.getY()); } @@ -129,12 +130,12 @@ public class QuadTree int radius = this.diameterInBlocks() / 2; DhBlockPos2D minPos = this.getCenterBlockPos().add(new DhBlockPos2D(-radius, -radius)); DhBlockPos2D maxPos = this.getCenterBlockPos().add(new DhBlockPos2D(radius, radius)); - throw new IndexOutOfBoundsException("QuadTree GetOrSet failed. Position out of bounds, min pos: " + minPos + ", max pos: " + maxPos + ", min detail level: " + this.treeMaxDetailLevel + ", max detail level: " + this.treeMinDetailLevel + ". Given Position: [" + DhSectionPos.toString(pos) + "] = block pos: " + DhSectionPos.convertToDetailLevel(pos, LodUtil.BLOCK_DETAIL_LEVEL)); + throw new IndexOutOfBoundsException("QuadTree GetOrSet failed. Position out of bounds, min pos: " + minPos + ", max pos: " + maxPos + ", min detail level: " + this.treeLeafDetailLevel + ", max detail level: " + this.treeRootDetailLevel + ". Given Position: [" + DhSectionPos.toString(pos) + "] = block pos: " + DhSectionPos.convertToDetailLevel(pos, LodUtil.BLOCK_DETAIL_LEVEL)); } - long rootPos = DhSectionPos.convertToDetailLevel(pos, this.treeMinDetailLevel); + long rootPos = DhSectionPos.convertToDetailLevel(pos, this.treeRootDetailLevel); int ringListPosX = DhSectionPos.getX(rootPos); int ringListPosZ = DhSectionPos.getZ(rootPos); @@ -146,7 +147,7 @@ public class QuadTree return null; } - topQuadNode = new QuadNode(rootPos, this.treeMaxDetailLevel); + topQuadNode = new QuadNode(rootPos, this.treeLeafDetailLevel); boolean successfullyAdded = this.topRingList.set(ringListPosX, ringListPosZ, topQuadNode); if (!successfullyAdded) { @@ -171,7 +172,7 @@ public class QuadTree public boolean isSectionPosInBounds(long testPos) { // check if the testPos is within the detail level limits of the tree - boolean detailLevelWithinBounds = this.treeMaxDetailLevel <= DhSectionPos.getDetailLevel(testPos) && DhSectionPos.getDetailLevel(testPos) <= this.treeMinDetailLevel; + boolean detailLevelWithinBounds = this.treeLeafDetailLevel <= DhSectionPos.getDetailLevel(testPos) && DhSectionPos.getDetailLevel(testPos) <= this.treeRootDetailLevel; if (!detailLevelWithinBounds) { return false; @@ -237,10 +238,12 @@ public class QuadTree //===========// /** can include null nodes */ - public LongIterator rootNodePosIterator() { return new QuadTreeRootPosIterator(true); } + public LongIterator rootNodePosIterator() { return new QuadTreeRootPosIterator(true, null); } - public Iterator> nodeIterator() { return new QuadTreeNodeIterator(false); } - public Iterator> leafNodeIterator() { return new QuadTreeNodeIterator(true); } + /** @see INodeIteratorStoppingFunc */ + public Iterator> nodeIteratorWithStoppingFilter(INodeIteratorStoppingFunc stoppingFilterFunc) { return new QuadTreeNodeIterator(false, stoppingFilterFunc); } + public Iterator> nodeIterator() { return new QuadTreeNodeIterator(false, null); } + public Iterator> leafNodeIterator() { return new QuadTreeNodeIterator(true, null); } @@ -254,8 +257,8 @@ public class QuadTree this.centerBlockPos = newCenterPos; Pos2D expectedCenterPos = new Pos2D( - BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, this.treeMinDetailLevel), - BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, this.treeMinDetailLevel)); + BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, this.treeRootDetailLevel), + BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, this.treeRootDetailLevel)); if (this.topRingList.getCenter().equals(expectedCenterPos)) { @@ -320,16 +323,14 @@ public class QuadTree - - //==============// // base methods // //==============// - public boolean isEmpty() { return this.count() == 0; } // TODO this should be rewritten to short-circuit + public boolean isEmpty() { return this.nodeCount() == 0; } // this should be rewritten to short-circuit /** @return the number of non-null nodes in the tree */ - public int count() + public int nodeCount() { int count = 0; for (QuadNode node : this.topRingList) @@ -394,7 +395,13 @@ public class QuadTree // } @Override - public String toString() { return "center block: " + this.centerBlockPos + ", block width: " + this.diameterInBlocks + ", detail level range: [" + this.treeMaxDetailLevel + "-" + this.treeMinDetailLevel + "], leaf #: " + this.leafNodeCount(); } + public String toString() + { + return "center block: " + this.centerBlockPos + + ", block width: " + this.diameterInBlocks + + ", detail level range: [" + this.treeLeafDetailLevel + "-" + this.treeRootDetailLevel + "], " + + "leaf #: " + this.leafNodeCount(); + } @@ -402,19 +409,39 @@ public class QuadTree // iterator classes // //==================// + /** @see INodeIteratorStoppingFunc#iteratorShouldStop(QuadNode) */ + @FunctionalInterface + public interface INodeIteratorStoppingFunc + { + /** if this function returns true then the iterator will stop walking down the tree */ + boolean iteratorShouldStop(QuadNode node); + } + private class QuadTreeRootPosIterator implements LongIterator { - private final LongArrayFIFOQueue iteratorPosQueue = new LongArrayFIFOQueue(); + private final LongArrayFIFOQueue iteratorPosQueue; + @Nullable + private final INodeIteratorStoppingFunc stopIteratingFunc; - public QuadTreeRootPosIterator(boolean includeNullNodes) + public QuadTreeRootPosIterator(boolean includeNullNodes, @Nullable INodeIteratorStoppingFunc stopIteratingFunc) { + this.iteratorPosQueue = new LongArrayFIFOQueue(); + this.stopIteratingFunc = stopIteratingFunc; + QuadTree.this.topRingList.forEachPosOrdered((node, pos2D) -> { - if (node != null || includeNullNodes) + if (this.stopIteratingFunc != null + && this.stopIteratingFunc.iteratorShouldStop(node)) { - long rootPos = DhSectionPos.encode(QuadTree.this.treeMinDetailLevel, pos2D.getX(), pos2D.getY()); + return; + } + + if (node != null + || includeNullNodes) + { + long rootPos = DhSectionPos.encode(QuadTree.this.treeRootDetailLevel, pos2D.getX(), pos2D.getY()); if (QuadTree.this.isSectionPosInBounds(rootPos)) { this.iteratorPosQueue.enqueue(rootPos); @@ -459,13 +486,17 @@ public class QuadTree private QuadNode lastNode = null; private final boolean onlyReturnLeaves; + @Nullable + private final INodeIteratorStoppingFunc stopIteratingFunc; - public QuadTreeNodeIterator(boolean onlyReturnLeaves) + public QuadTreeNodeIterator(boolean onlyReturnLeaves, @Nullable INodeIteratorStoppingFunc stopIteratingFunc) { - this.rootNodeIterator = new QuadTreeRootPosIterator(false); + this.rootNodeIterator = new QuadTreeRootPosIterator(false, stopIteratingFunc); this.onlyReturnLeaves = onlyReturnLeaves; + + this.stopIteratingFunc = stopIteratingFunc; } @@ -473,23 +504,28 @@ public class QuadTree @Override public boolean hasNext() { - if (!this.rootNodeIterator.hasNext() && this.currentNodeIterator != null && !this.currentNodeIterator.hasNext()) + if (!this.rootNodeIterator.hasNext() + && this.currentNodeIterator != null + && !this.currentNodeIterator.hasNext()) { return false; } - if (this.currentNodeIterator == null || !this.currentNodeIterator.hasNext()) + if (this.currentNodeIterator == null + || !this.currentNodeIterator.hasNext()) { this.currentNodeIterator = this.getNextChildNodeIterator(); } + return this.currentNodeIterator != null && this.currentNodeIterator.hasNext(); } @Override public QuadNode next() { - if (this.currentNodeIterator == null || !this.currentNodeIterator.hasNext()) + if (this.currentNodeIterator == null + || !this.currentNodeIterator.hasNext()) { this.currentNodeIterator = this.getNextChildNodeIterator(); } @@ -503,13 +539,14 @@ public class QuadTree private Iterator> getNextChildNodeIterator() { Iterator> nodeIterator = null; - while ((nodeIterator == null || !nodeIterator.hasNext()) && this.rootNodeIterator.hasNext()) + while ((nodeIterator == null || !nodeIterator.hasNext()) + && this.rootNodeIterator.hasNext()) { long sectionPos = this.rootNodeIterator.nextLong(); QuadNode rootNode = QuadTree.this.getNode(sectionPos); if (rootNode != null) { - nodeIterator = this.onlyReturnLeaves ? rootNode.getLeafNodeIterator() : rootNode.getNodeIterator(); + nodeIterator = this.onlyReturnLeaves ? rootNode.getLeafNodeIterator() : rootNode.getNodeIterator(this.stopIteratingFunc); } } return nodeIterator; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/iterators/QuadNodeChildIndexIterator.java b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/iterators/QuadNodeChildIndexIterator.java index 8ee9dc20f..eea645597 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/iterators/QuadNodeChildIndexIterator.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/iterators/QuadNodeChildIndexIterator.java @@ -34,7 +34,7 @@ public class QuadNodeChildIndexIterator implements Iterator public QuadNodeChildIndexIterator(QuadNode parentNode, boolean returnNullChildPos) { // only get the children if this section isn't at the bottom of the tree - if (DhSectionPos.getDetailLevel(parentNode.sectionPos) > parentNode.minimumDetailLevel) + if (DhSectionPos.getDetailLevel(parentNode.sectionPos) > parentNode.parentTreeLeafDetailLevel) { // go over each child pos for (int i = 0; i < 4; i++) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/iterators/QuadTreeNodeIterator.java b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/iterators/QuadTreeNodeIterator.java index be7785894..09b3d5691 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/iterators/QuadTreeNodeIterator.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/iterators/QuadTreeNodeIterator.java @@ -21,6 +21,8 @@ package com.seibel.distanthorizons.core.util.objects.quadTree.iterators; import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.util.objects.quadTree.QuadNode; +import com.seibel.distanthorizons.core.util.objects.quadTree.QuadTree; +import org.jetbrains.annotations.Nullable; import java.util.*; import java.util.function.Consumer; @@ -36,31 +38,36 @@ public class QuadTreeNodeIterator implements Iterator> private byte iteratorDetailLevel = 0; private final boolean onlyReturnLeafValues; + @Nullable + private final QuadTree.INodeIteratorStoppingFunc stopIteratingFunc; - - public QuadTreeNodeIterator(QuadNode rootNode, boolean onlyReturnLeafValues) + public QuadTreeNodeIterator( + QuadNode rootNode, + boolean onlyReturnLeafValues, @Nullable QuadTree.INodeIteratorStoppingFunc stopIteratingFunc) { this.onlyReturnLeafValues = onlyReturnLeafValues; + this.stopIteratingFunc = stopIteratingFunc; // TODO the naming conversion for these are flipped in a lot of places - this.highestDetailLevel = rootNode.minimumDetailLevel; + this.highestDetailLevel = rootNode.parentTreeLeafDetailLevel; this.iteratorDetailLevel = DhSectionPos.getDetailLevel(rootNode.sectionPos); if (!this.onlyReturnLeafValues) { + // return all nodes + // set the start for the iterator this.validNodesForDetailLevel.add(rootNode); this.iteratorNodeQueue.add(rootNode); } else { - // fully populate the iterator + // return leaf nodes + // fully populate the iterator // this isn't the best way to do this, especially for large trees, // but it is simple and functions well enough for now - - Queue> parentNodeQueue = new ArrayDeque<>(); parentNodeQueue.add(rootNode); @@ -68,6 +75,13 @@ public class QuadTreeNodeIterator implements Iterator> while (parentNodeQueue.peek() != null) { QuadNode parentNode = parentNodeQueue.poll(); + + if (stopIteratingFunc != null + && stopIteratingFunc.iteratorShouldStop(parentNode)) + { + continue; + } + for (int i = 0; i < 4; i++) { QuadNode childNode = parentNode.getChildByIndex(i); @@ -112,7 +126,8 @@ public class QuadTreeNodeIterator implements Iterator> // the iterator queue should be fully populated for leaf values, // for all values, it needs to be populated for each detail level - if (this.iteratorNodeQueue.size() == 0 && !onlyReturnLeafValues) + if (this.iteratorNodeQueue.size() == 0 + && !this.onlyReturnLeafValues) { // populate the next detail level list @@ -123,17 +138,32 @@ public class QuadTreeNodeIterator implements Iterator> Queue> parentNodes = new LinkedList<>(this.validNodesForDetailLevel); this.validNodesForDetailLevel.clear(); - // populate the list of nodes for this level + // populate the list of nodes for this detail level for (QuadNode parentNode : parentNodes) { + if (this.stopIteratingFunc != null + && this.stopIteratingFunc.iteratorShouldStop(parentNode)) + { + continue; + } + + for (int i = 0; i < 4; i++) { QuadNode childNode = parentNode.getChildByIndex(i); - if (childNode != null) + if (childNode == null) { - this.iteratorNodeQueue.add(childNode); - this.validNodesForDetailLevel.add(childNode); + continue; } + + if (this.stopIteratingFunc != null + && this.stopIteratingFunc.iteratorShouldStop(childNode)) + { + continue; + } + + this.iteratorNodeQueue.add(childNode); + this.validNodesForDetailLevel.add(childNode); } } } diff --git a/core/src/test/java/tests/QuadTreeTest.java b/core/src/test/java/tests/QuadTreeTest.java index 310b78aba..4c82824db 100644 --- a/core/src/test/java/tests/QuadTreeTest.java +++ b/core/src/test/java/tests/QuadTreeTest.java @@ -31,9 +31,11 @@ import it.unimi.dsi.fastutil.longs.LongIterator; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.core.config.Configurator; +import org.jetbrains.annotations.Nullable; import org.junit.Assert; import org.junit.Test; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.concurrent.atomic.AtomicInteger; @@ -56,7 +58,7 @@ public class QuadTreeTest { AbstractTestTreeParams treeParams = new LargeTestTree(); QuadTree tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL); - Assert.assertTrue("Tree min/max detail level out of expected bounds: " + tree, tree.treeMinDetailLevel >= 10 && tree.treeMaxDetailLevel <= 10 - 4); + Assert.assertTrue("Tree min/max detail level out of expected bounds: " + tree, tree.treeRootDetailLevel >= 10 && tree.treeLeafDetailLevel <= 10 - 4); // (pseudo) root node // @@ -171,7 +173,7 @@ public class QuadTreeTest { // very specific tree parameters to match test results QuadTree tree = new QuadTree<>(512, new DhBlockPos2D(125, -516), (byte) 6); - Assert.assertEquals("Test may need to be re-calculated for different max detail level.", 9, tree.treeMinDetailLevel); + Assert.assertEquals("Test may need to be re-calculated for different max detail level.", 9, tree.treeRootDetailLevel); long rootPos = DhSectionPos.encode((byte) 9, 0, -1); @@ -329,6 +331,125 @@ public class QuadTreeTest } + @Test + public void QuadTreeIterationFilterTest() + { + AbstractTestTreeParams treeParams = new TinyTestTree(); + QuadTree tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), (byte)0); + + + + // fill tree root node(s) + int rootNodeCount = 0; + LongIterator rootNodePosIterator = tree.rootNodePosIterator(); + while (rootNodePosIterator.hasNext()) + { + long rootNodePos = rootNodePosIterator.nextLong(); + recursivelyFillRootNodeTree(tree, (byte)0, rootNodePos); + + rootNodeCount++; + } + + Assert.assertEquals("Only one root node expected", 1, rootNodeCount); + + // confirm tree isn't empty + int totalNodeCount = tree.nodeCount(); + Assert.assertNotEquals("No nodes", 0, totalNodeCount); + int leafCount = tree.leafNodeCount(); + Assert.assertNotEquals("No leaf nodes", 0, leafCount); + + // confirm tree has correct number of nodes + int treeDepth = tree.treeRootDetailLevel - tree.treeLeafDetailLevel; + + // equation source: http://www.gamedev.net/forums/topic/325071-calculate-number-of-nodes-in-a-quad-tree/3098433/ + int expectedNodeCount = (int)Math.pow(4, treeDepth + 1); + expectedNodeCount = (expectedNodeCount - 1) / 3; + + Assert.assertEquals("Unexpected total node count", expectedNodeCount, totalNodeCount); + int expectedLeafNodeCount = (int)Math.pow(4, treeDepth); + Assert.assertEquals("Unexpected leaf node count", expectedLeafNodeCount, leafCount); + + + + // filters // + + // accept everything + assertFilterCount(tree, "Get everything should return total node count", totalNodeCount, (node) -> false); + // ignore everything + assertFilterCount(tree, "Ignore everything filter shouldn't return anything", 0, (node) -> true); + + // root detail level + assertFilterCount(tree, "filter root detail", 1, + (node) -> + { + if (node == null) + { + return true; + } + + return DhSectionPos.getDetailLevel(node.sectionPos) < tree.treeRootDetailLevel; + }); + + // root - 1 detail level + assertFilterCount(tree, "filter root-1 detail", 1 + 4, + (node) -> + { + if (node == null) + { + return true; + } + + return DhSectionPos.getDetailLevel(node.sectionPos) < tree.treeRootDetailLevel - 1; + }); + + // only nodes in (4*0,0) + assertFilterCount(tree, "filter to root-1 NW corner, should return 1/4th of all nodes plus root ", (totalNodeCount/4) + 1, + (node) -> + { + if (node == null) + { + return true; + } + + return !DhSectionPos.contains(DhSectionPos.encode((byte)4, 0, 0), node.sectionPos); + }); + + + + } + private static void assertFilterCount(QuadTree tree, String message, int expectedNodeCount, @Nullable QuadTree.INodeIteratorStoppingFunc stoppingFilterFunc) // TODO functional interface + { + ArrayList foundNodePositionStrings = new ArrayList<>(); + + int filteredNodeCount = 0; + Iterator> filteredNodeIterator = tree.nodeIteratorWithStoppingFilter(stoppingFilterFunc); + while (filteredNodeIterator.hasNext()) + { + QuadNode node = filteredNodeIterator.next(); + foundNodePositionStrings.add(DhSectionPos.toString(node.sectionPos)); + filteredNodeCount++; + } + + Assert.assertEquals(message + " | found count: ["+foundNodePositionStrings.size()+"], found nodes: ["+ String.join(", ", foundNodePositionStrings)+"]", expectedNodeCount, filteredNodeCount); + } + private static void recursivelyFillRootNodeTree(QuadTree tree, byte bottomDetailLevel, long rootNodePos) + { + byte thisDetailLevel = DhSectionPos.getDetailLevel(rootNodePos); + tree.setValue(rootNodePos, (int)thisDetailLevel); + + if (thisDetailLevel <= bottomDetailLevel) + { + return; + } + + for (int i = 0; i < 4; i++) + { + long childPos = DhSectionPos.getChildByIndex(rootNodePos, i); + recursivelyFillRootNodeTree(tree, bottomDetailLevel, childPos); + } + } + + @Test public void NewQuadTreeIterationTest() { @@ -411,7 +532,7 @@ public class QuadTreeTest { AbstractTestTreeParams treeParams = new TinyTestTree(); final QuadTree tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL); - testSet(tree, DhSectionPos.encode(tree.treeMinDetailLevel, 0, 0), 0); + testSet(tree, DhSectionPos.encode(tree.treeRootDetailLevel, 0, 0), 0); // confirm the root node were added int rootNodeCount = 0; @@ -499,10 +620,10 @@ public class QuadTreeTest Assert.assertEquals("incorrect tree width", treeParams.getWidthInBlocks(), tree.diameterInBlocks()); - testSet(tree, DhSectionPos.encode(tree.treeMinDetailLevel, 0, 0), 0); + testSet(tree, DhSectionPos.encode(tree.treeRootDetailLevel, 0, 0), 0); - testSet(tree, DhSectionPos.encode(tree.treeMinDetailLevel, -1, -1), -1, IndexOutOfBoundsException.class); - testSet(tree, DhSectionPos.encode(tree.treeMinDetailLevel, 1, 1), -1, IndexOutOfBoundsException.class); + testSet(tree, DhSectionPos.encode(tree.treeRootDetailLevel, -1, -1), -1, IndexOutOfBoundsException.class); + testSet(tree, DhSectionPos.encode(tree.treeRootDetailLevel, 1, 1), -1, IndexOutOfBoundsException.class); int rootNodeCount = 0; LongIterator rootNodeIterator = tree.rootNodePosIterator(); @@ -529,18 +650,18 @@ public class QuadTreeTest // 2x2 valid positions (overlap the tree's width) - testSet(tree, DhSectionPos.encode(tree.treeMinDetailLevel, 0, 0), 0); - testSet(tree, DhSectionPos.encode(tree.treeMinDetailLevel, -1, 0), 0); - testSet(tree, DhSectionPos.encode(tree.treeMinDetailLevel, 0, -1), 0); - testSet(tree, DhSectionPos.encode(tree.treeMinDetailLevel, -1, -1), 0); + testSet(tree, DhSectionPos.encode(tree.treeRootDetailLevel, 0, 0), 0); + testSet(tree, DhSectionPos.encode(tree.treeRootDetailLevel, -1, 0), 0); + testSet(tree, DhSectionPos.encode(tree.treeRootDetailLevel, 0, -1), 0); + testSet(tree, DhSectionPos.encode(tree.treeRootDetailLevel, -1, -1), 0); // invalid positions - testSet(tree, DhSectionPos.encode(tree.treeMinDetailLevel, -1, 1), -1, IndexOutOfBoundsException.class); - testSet(tree, DhSectionPos.encode(tree.treeMinDetailLevel, 0, 1), -1, IndexOutOfBoundsException.class); + testSet(tree, DhSectionPos.encode(tree.treeRootDetailLevel, -1, 1), -1, IndexOutOfBoundsException.class); + testSet(tree, DhSectionPos.encode(tree.treeRootDetailLevel, 0, 1), -1, IndexOutOfBoundsException.class); - testSet(tree, DhSectionPos.encode(tree.treeMinDetailLevel, 1, 0), -1, IndexOutOfBoundsException.class); - testSet(tree, DhSectionPos.encode(tree.treeMinDetailLevel, 1, 1), -1, IndexOutOfBoundsException.class); - testSet(tree, DhSectionPos.encode(tree.treeMinDetailLevel, 1, -1), -1, IndexOutOfBoundsException.class); + testSet(tree, DhSectionPos.encode(tree.treeRootDetailLevel, 1, 0), -1, IndexOutOfBoundsException.class); + testSet(tree, DhSectionPos.encode(tree.treeRootDetailLevel, 1, 1), -1, IndexOutOfBoundsException.class); + testSet(tree, DhSectionPos.encode(tree.treeRootDetailLevel, 1, -1), -1, IndexOutOfBoundsException.class); int rootNodeCount = 0; @@ -562,7 +683,7 @@ public class QuadTreeTest { AbstractTestTreeParams treeParams = new MediumTestTree(); QuadTree tree = new QuadTree<>(treeParams.getWidthInBlocks(), new DhBlockPos2D(0, 0), (byte) 8); - Assert.assertEquals("Test detail level's need to be adjusted. This isn't necessarily a failed test.", 10, tree.treeMinDetailLevel); + Assert.assertEquals("Test detail level's need to be adjusted. This isn't necessarily a failed test.", 10, tree.treeRootDetailLevel); // valid detail levels testSet(tree, DhSectionPos.encode((byte) 10, 0, 0), 1); @@ -584,14 +705,14 @@ public class QuadTreeTest { AbstractTestTreeParams treeParams = new MediumTestTree(); QuadTree tree = new QuadTree<>(treeParams.getWidthInBlocks(), new DhBlockPos2D(0, 0), (byte) 6); - Assert.assertEquals("Test detail level's need to be adjusted. This isn't necessarily a failed test.", 10, tree.treeMinDetailLevel); + Assert.assertEquals("Test detail level's need to be adjusted. This isn't necessarily a failed test.", 10, tree.treeRootDetailLevel); // create the root node testSet(tree, DhSectionPos.encode((byte) 10, 0, 0), 1); - AtomicInteger minimumDetailLevelReachedRef = new AtomicInteger(tree.treeMinDetailLevel); + AtomicInteger minimumDetailLevelReachedRef = new AtomicInteger(tree.treeRootDetailLevel); // recurse down the tree LongIterator rootNodePosIterator = tree.rootNodePosIterator(); @@ -618,13 +739,13 @@ public class QuadTreeTest QuadNode childNode = ChildIterator.next(); Assert.assertNotNull(childNode); // TODO is this correct? - this.recursivelyCreateNodeChildren(childNode, tree.treeMaxDetailLevel, minimumDetailLevelReachedRef); + this.recursivelyCreateNodeChildren(childNode, tree.treeLeafDetailLevel, minimumDetailLevelReachedRef); } } } // confirm that the tree can and did iterate all the way down to the minimum detail level - Assert.assertEquals("Incorrect minimum detail level reached.", tree.treeMaxDetailLevel, minimumDetailLevelReachedRef.get()); + Assert.assertEquals("Incorrect minimum detail level reached.", tree.treeLeafDetailLevel, minimumDetailLevelReachedRef.get()); } private void recursivelyCreateNodeChildren(QuadNode node, byte minDetailLevel, AtomicInteger minimumDetailLevelReachedRef) { @@ -773,9 +894,9 @@ public class QuadTreeTest // testSet(tree, DhSectionPos.encode((byte) 0, 0, 0), 1); - Assert.assertEquals(1, tree.count()); + Assert.assertEquals(1, tree.nodeCount()); tree.setCenterBlockPos(new DhBlockPos2D(treeWidth + (treeWidth / 2), 0)); - Assert.assertEquals(0, tree.count()); + Assert.assertEquals(0, tree.nodeCount()); } @@ -899,9 +1020,10 @@ public class QuadTreeTest /** the tree should be slightly larger than the width in blocks to account for offset centers */ public int getWidthInRootNodes() { return MathUtil.log2(this.getWidthInBlocks()) + 2; } - public byte getMaxDetailLevel() { return (byte) MathUtil.log2(this.getWidthInBlocks()); } + /** the top (root) detail level in the tree */ + public byte getMinDetailLevel() { return (byte) MathUtil.log2(this.getWidthInBlocks()); } /** @return the block pos so that the tree's negative corner lines up with (0,0) */ - public DhBlockPos2D getPositiveEdgeCenterPos() { return new DhBlockPos2D(BitShiftUtil.powerOfTwo(this.getMaxDetailLevel()) / 2, BitShiftUtil.powerOfTwo(this.getMaxDetailLevel()) / 2); } + public DhBlockPos2D getPositiveEdgeCenterPos() { return new DhBlockPos2D(BitShiftUtil.powerOfTwo(this.getMinDetailLevel()) / 2, BitShiftUtil.powerOfTwo(this.getMinDetailLevel()) / 2); } }