run occlusion culling whenever saving a LOD

Also run culling for every column in an LOD, which improves compression by about 20%
- Thanks Scaevolus
This commit is contained in:
James Seibel
2025-10-22 07:25:04 -05:00
parent 939e45ce62
commit 06bef93c82
4 changed files with 262 additions and 161 deletions
@@ -100,7 +100,7 @@ public class FullDataPointIdMap
}
catch (IndexOutOfBoundsException e)
{
throw new IndexOutOfBoundsException("FullData ID Map out of sync for pos: "+this.pos+". ID: ["+id+"] greater than the number of known ID's: ["+this.entryList.size()+"].");
throw new IndexOutOfBoundsException("FullData ID Map out of sync for pos: "+DhSectionPos.toString(this.pos)+". ID: ["+id+"] greater than the number of known ID's: ["+this.entryList.size()+"].");
}
return entry;
@@ -23,7 +23,9 @@ import com.seibel.distanthorizons.api.enums.config.EDhApiWorldCompressionMode;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiWorldGenerationStep;
import com.seibel.distanthorizons.api.objects.data.DhApiTerrainDataPoint;
import com.seibel.distanthorizons.api.objects.data.IDhApiFullDataSource;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.FullDataPointIdMap;
import com.seibel.distanthorizons.core.dataObjects.transformers.FullDataOcclusionCuller;
import com.seibel.distanthorizons.core.dataObjects.transformers.LodDataBuilder;
import com.seibel.distanthorizons.core.file.AbstractDataSourceHandler;
import com.seibel.distanthorizons.core.file.IDataSource;
@@ -45,8 +47,8 @@ import it.unimi.dsi.fastutil.bytes.ByteArrayList;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -283,13 +285,25 @@ public class FullDataSourceV2
//======//
//=========//
// getters //
//======//
//=========//
public LongArrayList get(int relX, int relZ) throws IndexOutOfBoundsException
{ return this.dataPoints[relativePosToIndex(relX, relZ)]; }
@Nullable
public LongArrayList tryGet(int relX, int relZ)
{
int index = tryGetRelativePosToIndex(relX, relZ);
if (index == -1)
{
return null;
}
return this.dataPoints[index];
}
/**
* returns {@link FullDataPointUtil#EMPTY_DATA_POINT} if the given {@link DhBlockPos}
* is outside this data source's boundaries.
@@ -435,8 +449,31 @@ public class FullDataSourceV2
throw new UnsupportedOperationException("Unsupported data source update. Expected input detail level of ["+(thisDetailLevel-1)+"], ["+thisDetailLevel+"], or ["+(thisDetailLevel+1)+"], received detail level ["+inputDetailLevel+"].");
}
if (dataChanged)
{
EDhApiWorldCompressionMode worldCompressionMode = Config.Common.LodBuilding.worldCompression.get();
boolean cullHiddenBlocks = (worldCompressionMode != EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS);
if (cullHiddenBlocks)
{
for (int x = 0; x < WIDTH; x++)
{
for (int z = 0; z < WIDTH; z++)
{
LongArrayList dataColumn = this.get(x, z);
if (dataColumn != null
&& dataColumn.size() > 1)
{
FullDataOcclusionCuller.cullHiddenDatapointsInColumn(this, x, z);
}
}
}
}
// update the hash code
this.generateHashCode();
}
@@ -1072,19 +1109,36 @@ public class FullDataSourceV2
// helper methods //
//================//
/**
* Usually this should just be used internally, but there may be instances
* where the raw data arrays are available without the data source object.
*
* @return -1 if given an out-of-bounds relative position
*/
public static int tryGetRelativePosToIndex(int relX, int relZ)
{
if (relX < 0 || relZ < 0
|| relX >= WIDTH || relZ >= WIDTH)
{
return -1;
}
return (relX * WIDTH) + relZ;
}
/**
* Usually this should just be used internally, but there may be instances
* where the raw data arrays are available without the data source object.
*/
public static int relativePosToIndex(int relX, int relZ) throws IndexOutOfBoundsException
{
if (relX < 0 || relZ < 0 ||
relX > WIDTH || relZ > WIDTH)
int index = tryGetRelativePosToIndex(relX, relZ);
if (index < 0)
{
throw new IndexOutOfBoundsException("Relative data source positions must be between [0] and ["+WIDTH+"] (inclusive) the relative pos: ["+relX+","+relZ+"] is outside of those boundaries.");
throw new IndexOutOfBoundsException("Relative data source positions must be between [0] (inclusive) and ["+WIDTH+"] (exclusive) the relative pos: ["+relX+","+relZ+"] is outside those boundaries.");
}
return (relX * WIDTH) + relZ;
return index;
}
/**
@@ -0,0 +1,199 @@
package com.seibel.distanthorizons.core.dataObjects.transformers;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.util.FullDataPointUtil;
import com.seibel.distanthorizons.core.util.LodUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import org.jetbrains.annotations.NotNull;
public class FullDataOcclusionCuller
{
/**
* Mutates the given datasource so blocks that aren't visible
* (IE completely surrounded by other opaque blocks)
* are removed from the data column.
*
* @param dataSource
* @param relX relative X position in the datasource
* @param relZ relative Z position in the datasource
*/
public static void cullHiddenDatapointsInColumn(
FullDataSourceV2 dataSource,
int relX, int relZ
)
{
LongArrayList centerColumn = dataSource.get(relX, relZ);
LongArrayList posXColumn = dataSource.tryGet(relX + 1, relZ);
LongArrayList negXColumn = dataSource.tryGet(relX - 1, relZ);
LongArrayList posZColumn = dataSource.tryGet(relX, relZ + 1);
LongArrayList negZColumn = dataSource.tryGet(relX, relZ - 1);
if (posXColumn == null || posXColumn.size() == 0
|| negXColumn == null || negXColumn.size() == 0
|| posZColumn == null || posZColumn.size() == 0
|| negZColumn == null || negZColumn.size() == 0)
{
// if any adjacent columns are empty then we can't
// cull this column, since at least one side will be open
// to air/void
return;
}
int centerIndex = centerColumn.size() - 1;
int posXIndex = (posXColumn.size() - 1);
int negXIndex = (negXColumn.size() - 1);
int posZIndex = (posZColumn.size() - 1);
int negZIndex = (negZColumn.size() - 1);
for (; centerIndex >= 0; centerIndex--)
{
long currentPoint = centerColumn.getLong(centerIndex);
// Translucent data points are not eligible to be culled.
if (isTranslucent(dataSource, currentPoint))
{
continue;
}
// the top segment should never be culled.
if (centerIndex == 0
|| isTranslucent(dataSource, centerColumn.getLong(centerIndex - 1)))
{
continue;
}
// the bottom segment can sometimes be culled.
// assume it will not be seen from below,
// because this would imply the player is in the void.
if (centerIndex + 1 < centerColumn.size()
&& isTranslucent(dataSource, centerColumn.getLong(centerIndex + 1)))
{
continue;
}
// the lowest/bedrock segment should not be culled
if (centerIndex + 1 == centerColumn.size())
{
continue;
}
posXIndex = checkOcclusion(dataSource, currentPoint, posXColumn, posXIndex);
if (posXIndex < 0)
{
posXIndex = ~posXIndex;
continue;
}
negXIndex = checkOcclusion(dataSource, currentPoint, negXColumn, negXIndex);
if (negXIndex < 0)
{
negXIndex = ~negXIndex;
continue;
}
posZIndex = checkOcclusion(dataSource, currentPoint, posZColumn, posZIndex);
if (posZIndex < 0)
{
posZIndex = ~posZIndex;
continue;
}
negZIndex = checkOcclusion(dataSource, currentPoint, negZColumn, negZIndex);
if (negZIndex < 0)
{
negZIndex = ~negZIndex;
continue;
}
// Current point is fully surrounded. remove it.
centerColumn.removeLong(centerIndex);
// Make the above data point cover the area that the current point used to occupy.
// The element that was at `centerIndex - 1` is still at that position even after removal of centerIndex.
long above = centerColumn.getLong(centerIndex - 1);
above = FullDataPointUtil.setBottomY(above, FullDataPointUtil.getBottomY(currentPoint));
above = FullDataPointUtil.setHeight(above, FullDataPointUtil.getHeight(currentPoint) + FullDataPointUtil.getHeight(above));
centerColumn.set(centerIndex - 1, above);
}
}
/**
checks if centerPoint is "covered" by opaque data points in adjacentColumn.
centerPoint counts as covered if, and only if, for all Y levels in its height range,
there exists an opaque data point in adjacentColumn which overlaps with that Y level.
@param source used to lookup blocks (and their opacities) based on their IDs.
@param centerPoint the point being checked to see if it's fully covered.
@param adjacentColumn the data points which might cover centerPoint.
@param adjacentIndex the starting index in adjacentColumn to start scanning at.
indices greater than adjacentIndex have already been checked and confirmed to
not overlap or only overlap partially with centerPoint's Y range.
@return if centerPoint is covered, returns the index of the segment which finishes covering it.
the start of the covering may be a smaller index. in this case, the returned index may be used
as the adjacentIndex provided to this method on the next iteration which yields a new centerPoint.
if centerPoint is NOT covered, returns the bitwise negation of the index of the
segment which did not cover it. this guarantees that the returned value is negative.
the caller should check for negative return values and manually un-negate them to proceed with the loop.
in other words, this function returns the index of the next adjacent data
point to use in the loop, AND a boolean indicating whether or not the
centerPoint is covered; both are packed into the same int, and returned.
*/
private static int checkOcclusion(@NotNull FullDataSourceV2 source, long centerPoint, @NotNull LongArrayList adjacentColumn, int adjacentIndex)
{
// check if this point is adjacent to an empty column
// if so it will always be shown
if (adjacentColumn.isEmpty())
{
return ~adjacentIndex;
}
else if (adjacentColumn.size() == 1
&& adjacentColumn.getLong(0) == FullDataPointUtil.EMPTY_DATA_POINT)
{
return ~adjacentIndex;
}
int bottomOfCenter = FullDataPointUtil.getBottomY(centerPoint);
int topOfCenter = bottomOfCenter + FullDataPointUtil.getHeight(centerPoint);
for (; adjacentIndex >= 0; adjacentIndex--)
{
long adjacentPoint = adjacentColumn.getLong(adjacentIndex);
int topOfAdjacent = FullDataPointUtil.getBottomY(adjacentPoint) + FullDataPointUtil.getHeight(adjacentPoint);
if (topOfAdjacent <= bottomOfCenter)
{
// the adjacent point is below the center point,
// check the next one
continue;
}
else if (isTranslucent(source, adjacentPoint))
{
// this point is adjacent to a transparent LOD and should be shown
return ~adjacentIndex;
}
else if (topOfAdjacent >= topOfCenter)
{
// the adjacent point covers the center point
return adjacentIndex;
}
}
// the Adjacent column ends before center column does,
// this point should be visible
return ~adjacentIndex;
}
private static boolean isTranslucent(FullDataSourceV2 source, long point)
{
int id = FullDataPointUtil.getId(point);
int opacity = source.mapping.getBlockStateWrapper(id).getOpacity();
return opacity < LodUtil.BLOCK_FULLY_OPAQUE;
}
}
@@ -47,6 +47,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import com.seibel.distanthorizons.core.logging.DhLogger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
public class LodDataBuilder
@@ -118,7 +119,6 @@ public class LodDataBuilder
//==========================//
EDhApiWorldCompressionMode worldCompressionMode = Config.Common.LodBuilding.worldCompression.get();
boolean ignoreHiddenBlocks = (worldCompressionMode != EDhApiWorldCompressionMode.MERGE_SAME_BLOCKS);
try
{
@@ -257,11 +257,6 @@ public class LodDataBuilder
dataSource.setSingleColumn(longs, columnX, columnZ, EDhApiWorldGenerationStep.LIGHT, worldCompressionMode);
}
}
if (ignoreHiddenBlocks)
{
cullHiddenBlocks(dataSource, chunkOffsetX, chunkOffsetZ);
}
}
catch (DataCorruptedException e)
{
@@ -273,153 +268,6 @@ public class LodDataBuilder
return dataSource;
}
private static void cullHiddenBlocks(FullDataSourceV2 dataSource, int chunkOffsetX, int chunkOffsetZ)
{
for (int relZ = 1; relZ < LodUtil.CHUNK_WIDTH - 1; relZ++)
{
for (int relX = 1; relX < LodUtil.CHUNK_WIDTH - 1; relX++)
{
LongArrayList
centerColumn = dataSource.get(relX + chunkOffsetX, relZ + chunkOffsetZ),
posXColumn = dataSource.get(relX + chunkOffsetX + 1, relZ + chunkOffsetZ),
negXColumn = dataSource.get(relX + chunkOffsetX - 1, relZ + chunkOffsetZ),
posZColumn = dataSource.get(relX + chunkOffsetX, relZ + chunkOffsetZ + 1),
negZColumn = dataSource.get(relX + chunkOffsetX, relZ + chunkOffsetZ - 1);
int
centerIndex = centerColumn.size() - 1,
posXIndex = posXColumn.size() - 1,
negXIndex = negXColumn.size() - 1,
posZIndex = posZColumn.size() - 1,
negZIndex = negZColumn.size() - 1;
for (; centerIndex >= 0; centerIndex--)
{
long currentPoint = centerColumn.getLong(centerIndex);
// Translucent data points are not eligible to be culled.
if (isTranslucent(dataSource, currentPoint))
{
continue;
}
// the top segment should never be culled.
if (centerIndex == 0
|| isTranslucent(dataSource, centerColumn.getLong(centerIndex - 1))
)
{
continue;
}
// the bottom segment can sometimes be culled.
// assume it will not be seen from below,
// because this would imply the player is in the void.
if (centerIndex + 1 < centerColumn.size()
&& isTranslucent(dataSource, centerColumn.getLong(centerIndex + 1))
)
{
continue;
}
// the lowest/bedrock segment should not be culled
if (centerIndex + 1 == centerColumn.size())
{
continue;
}
posXIndex = checkOcclusion(dataSource, currentPoint, posXColumn, posXIndex);
if (posXIndex < 0)
{
posXIndex = ~posXIndex;
continue;
}
negXIndex = checkOcclusion(dataSource, currentPoint, negXColumn, negXIndex);
if (negXIndex < 0)
{
negXIndex = ~negXIndex;
continue;
}
posZIndex = checkOcclusion(dataSource, currentPoint, posZColumn, posZIndex);
if (posZIndex < 0)
{
posZIndex = ~posZIndex;
continue;
}
negZIndex = checkOcclusion(dataSource, currentPoint, negZColumn, negZIndex);
if (negZIndex < 0)
{
negZIndex = ~negZIndex;
continue;
}
// Current point is fully surrounded. remove it.
centerColumn.removeLong(centerIndex);
// Make the above data point cover the area that the current point used to occupy.
// The element that was at `centerIndex - 1` is still at that position even after removal of centerIndex.
long above = centerColumn.getLong(centerIndex - 1);
above = FullDataPointUtil.setBottomY(above, FullDataPointUtil.getBottomY(currentPoint));
above = FullDataPointUtil.setHeight(above, FullDataPointUtil.getHeight(currentPoint) + FullDataPointUtil.getHeight(above));
centerColumn.set(centerIndex - 1, above);
}
}
}
}
/**
checks if centerPoint is "covered" by opaque data points in adjacentColumn.
centerPoint counts as covered if, and only if, for all Y levels in its height range,
there exists an opaque data point in adjacentColumn which overlaps with that Y level.
@param source used to lookup blocks (and their opacities) based on their IDs.
@param centerPoint the point being checked to see if it's fully covered.
@param adjacentColumn the data points which might cover centerPoint.
@param adjacentIndex the starting index in adjacentColumn to start scanning at.
indices greater than adjacentIndex have already been checked and confirmed to
not overlap or only overlap partially with centerPoint's Y range.
@return if centerPoint is covered, returns the index of the segment which finishes covering it.
the start of the covering may be a smaller index. in this case, the returned index may be used
as the adjacentIndex provided to this method on the next iteration which yields a new centerPoint.
if centerPoint is NOT covered, returns the bitwise negation of the index of the
segment which did not cover it. this guarantees that the returned value is negative.
the caller should check for negative return values and manually un-negate them to proceed with the loop.
in other words, this function returns the index of the next adjacent data
point to use in the loop, AND a boolean indicating whether or not the
centerPoint is covered; both are packed into the same int, and returned.
*/
private static int checkOcclusion(FullDataSourceV2 source, long centerPoint, LongArrayList adjacentColumn, int adjacentIndex)
{
int bottomOfCenter = FullDataPointUtil.getBottomY(centerPoint);
int topOfCenter = bottomOfCenter + FullDataPointUtil.getHeight(centerPoint);
for (; adjacentIndex >= 0; adjacentIndex--)
{
long adjacentPoint = adjacentColumn.getLong(adjacentIndex);
int topOfAdjacent = FullDataPointUtil.getBottomY(adjacentPoint) + FullDataPointUtil.getHeight(adjacentPoint);
if (topOfAdjacent <= bottomOfCenter)
{
continue;
}
else if (isTranslucent(source, adjacentPoint))
{
return ~adjacentIndex;
}
else if (topOfAdjacent >= topOfCenter)
{
return adjacentIndex;
}
}
throw new LodUtil.AssertFailureException("Adjacent column ends before center column does.");
}
private static boolean isTranslucent(FullDataSourceV2 source, long point) {
return source.mapping.getBlockStateWrapper(FullDataPointUtil.getId(point)).getOpacity() < LodUtil.BLOCK_FULLY_OPAQUE;
}
/** @throws ClassCastException if an API user returns the wrong object type(s) */