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:
+1
-1
@@ -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;
|
||||
|
||||
+61
-7
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+199
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
+1
-153
@@ -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) */
|
||||
|
||||
Reference in New Issue
Block a user