diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java index 8e2edcd90..82fabbfc6 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java @@ -200,23 +200,14 @@ public class LodDataBuilder // save the biome/block change if (!newBiome.equals(biome) || !newBlockState.equals(blockState)) { - // if we ignore hidden blocks, don't save this biome/block change - // wait until the block is visible and then save the new datapoint - if (!ignoreHiddenBlocks - // if the last block is air, this block will always be visible - || blockState.isAir() - // check if this block is visible from any direction - || blockVisible(chunkWrapper, relBlockX, y, relBlockZ)) - { - longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getInclusiveMinBuildHeight(), blockLight, skyLight)); - biome = newBiome; - blockState = newBlockState; - - mappedId = dataSource.mapping.addIfNotPresentAndGetId(biome, blockState); - blockLight = newBlockLight; - skyLight = newSkyLight; - lastY = y; - } + longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getInclusiveMinBuildHeight(), blockLight, skyLight)); + biome = newBiome; + blockState = newBlockState; + + mappedId = dataSource.mapping.addIfNotPresentAndGetId(biome, blockState); + blockLight = newBlockLight; + skyLight = newSkyLight; + lastY = y; } } longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getInclusiveMinBuildHeight(), blockLight, skyLight)); @@ -228,6 +219,11 @@ public class LodDataBuilder worldCompressionMode); } } + + if (ignoreHiddenBlocks) + { + cullHiddenBlocks(dataSource, chunkOffsetX, chunkOffsetZ); + } } catch (DataCorruptedException e) { @@ -238,67 +234,147 @@ public class LodDataBuilder LodUtil.assertTrue(!dataSource.isEmpty); return dataSource; } - private static boolean blockVisible(IChunkWrapper chunkWrapper, int relBlockX, int blockY, int relBlockZ) - { - DhBlockPos originalBlockPos = new DhBlockPos(relBlockX,blockY,relBlockZ); - final DhBlockPosMutable testBlockPos = new DhBlockPosMutable(relBlockX,blockY,relBlockZ); - - // up/down - if (blockInDirectionVisible(chunkWrapper, EDhDirection.UP, originalBlockPos, testBlockPos)) - { - return true; - } - if (blockInDirectionVisible(chunkWrapper, EDhDirection.DOWN, originalBlockPos, testBlockPos)) - { - return true; - } - - // north/south - if (blockInDirectionVisible(chunkWrapper, EDhDirection.NORTH, originalBlockPos, testBlockPos)) - { - return true; - } - if (blockInDirectionVisible(chunkWrapper, EDhDirection.SOUTH, originalBlockPos, testBlockPos)) - { - return true; - } - - // east/west - if (blockInDirectionVisible(chunkWrapper, EDhDirection.EAST, originalBlockPos, testBlockPos)) - { - return true; - } - if (blockInDirectionVisible(chunkWrapper, EDhDirection.WEST, originalBlockPos, testBlockPos)) - { - return true; - } - - - return false; - } - private static boolean blockInDirectionVisible(IChunkWrapper chunkWrapper, EDhDirection direction, DhBlockPos originalBlockPos, DhBlockPosMutable testBlockPos) - { - originalBlockPos.mutateOffset(direction, testBlockPos); - - // if the block is next to the border of a chunk, assume it's visible - if (testBlockPos.getX() < 0 || testBlockPos.getX() >= LodUtil.CHUNK_WIDTH) - { - return true; - } - if (testBlockPos.getZ() < 0 || testBlockPos.getZ() >= LodUtil.CHUNK_WIDTH) - { - return true; - } - if (testBlockPos.getY() < chunkWrapper.getInclusiveMinBuildHeight() || testBlockPos.getY() > chunkWrapper.getExclusiveMaxBuildHeight()) - { - return true; - } - - // this block isn't on a chunk boundary, check if it is next to a transparent/air block - IBlockStateWrapper blockState = chunkWrapper.getBlockState(testBlockPos); - return blockState.isAir() || blockState.getOpacity() != LodUtil.BLOCK_FULLY_OPAQUE; - } + 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; + } + + 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. + 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) */ public static FullDataSourceV2 createFromApiChunkData(DhApiChunk apiChunk, boolean runAdditionalValidation) throws ClassCastException, DataCorruptedException, IllegalArgumentException diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/FullDataPointUtil.java b/core/src/main/java/com/seibel/distanthorizons/core/util/FullDataPointUtil.java index 1b8fa5b5b..c561fe780 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/FullDataPointUtil.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/FullDataPointUtil.java @@ -159,6 +159,9 @@ public class FullDataPointUtil public static long setBlockLight(long data, byte blockLight) { return (data & ~((long) BLOCK_LIGHT_MASK << BLOCK_LIGHT_OFFSET) | (long) blockLight << BLOCK_LIGHT_OFFSET); } public static long setSkyLight(long data, int skyLight) { return (data & ~((long) SKY_LIGHT_MASK << SKY_LIGHT_OFFSET) | (long) skyLight << SKY_LIGHT_OFFSET); } + public static long setBottomY(long data, int bottomY) { return (data & ~(((long)(MIN_Y_MASK)) << MIN_Y_OFFSET)) | (((long)(bottomY)) << MIN_Y_OFFSET); } + public static long setHeight(long data, int height) { return (data & ~(((long)(HEIGHT_MASK)) << HEIGHT_OFFSET)) | (((long)(height)) << HEIGHT_OFFSET); } + public static String toString(long data) { return "[ID:" + getId(data) + ",Y:" + getBottomY(data) + ",Height:" + getHeight(data) + ",BlockLight:" + getBlockLight(data) + ",SkyLight:" + getSkyLight(data) + "]"; }