diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkFeatures.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkFeatures.java index 3a0eb132b..11fd7ba6c 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkFeatures.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkFeatures.java @@ -12,6 +12,7 @@ import art.arcane.iris.generation.hydrology.water.PondMask; import art.arcane.iris.generation.hydrology.water.Reach; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -28,17 +29,20 @@ final class ChunkFeatures { private final List ponds; private final List lakes; private final List inlets; + private final int[] inletBoxes; private final int[] lakeBoxes; private final ReachSegments segments = new ReachSegments(SEGMENT_CAPACITY); private final MaskEdges pondShores = new MaskEdges(MaskEdges.Kind.WATER); private final MaskEdges pondBanks = new MaskEdges(MaskEdges.Kind.BANK); private final MaskEdges lakeReach = new MaskEdges(MaskEdges.Kind.GROWN_WATER); + private boolean inert; - private ChunkFeatures(List reaches, List ponds, List lakes, List inlets) { + private ChunkFeatures(List reaches, List ponds, List lakes, List inlets, int[] inletBoxes) { this.reaches = reaches; this.ponds = ponds; this.lakes = lakes; this.inlets = inlets; + this.inletBoxes = inletBoxes; this.lakeBoxes = new int[4 * lakes.size()]; } @@ -48,12 +52,15 @@ final class ChunkFeatures { List reaches = new ArrayList<>(); List ponds = new ArrayList<>(); List inlets = new ArrayList<>(); + int[] inletBoxes = new int[0]; for (IndexedWindow window : windows) { for (SeaInlet inlet : window.plan().inlets()) { int[] bounds = SeaInletRasterizer.bounds(inlet); int padding = RiffleProfile.RADIUS + 1; if (bounds[0] <= chunkX * CHUNK + CHUNK - 1 + padding && bounds[2] >= chunkX * CHUNK - padding && bounds[1] <= chunkZ * CHUNK + CHUNK - 1 + padding && bounds[3] >= chunkZ * CHUNK - padding) { + inletBoxes = Arrays.copyOf(inletBoxes, inletBoxes.length + 4); + System.arraycopy(bounds, 0, inletBoxes, 4 * inlets.size(), 4); inlets.add(inlet); } } @@ -64,17 +71,22 @@ final class ChunkFeatures { } } List lakes = lakes(chunkX, chunkZ, halo, lakeRegionPlans, lakeRegions); - ChunkFeatures features = new ChunkFeatures(reaches, ponds, lakes, inlets); + ChunkFeatures features = new ChunkFeatures(reaches, ponds, lakes, inlets, inletBoxes); features.index(settings, chunkX, chunkZ); return features; } - boolean empty() { - return reaches.isEmpty() && ponds.isEmpty() && lakes.isEmpty() && inlets.isEmpty(); + boolean inert() { + return inert; } SeaInlet openInletAt(int x, int z, int[] column) { - for (SeaInlet inlet : inlets) { + for (int index = 0; index < inlets.size(); index++) { + int box = 4 * index; + if (x < inletBoxes[box] || z < inletBoxes[box + 1] || x > inletBoxes[box + 2] || z > inletBoxes[box + 3]) { + continue; + } + SeaInlet inlet = inlets.get(index); if (SeaInletRasterizer.column(inlet, x, z, column) && column[1] == SeaInlet.OPEN) { return inlet; } @@ -179,6 +191,8 @@ final class ChunkFeatures { for (Reach reach : reaches) { segments.addNear(reach, minX - 1f, minZ - 1f, maxX + 1f, maxZ + 1f, outward); } + int riffle = RiffleProfile.RADIUS + 1; + segments.coverChannels(minX - riffle, minZ - riffle, maxX + riffle, maxZ + riffle); HydrologySettings.PondShore shore = settings.ponds().shore(); int reach = Math.max(shore.taper(), ChunkWaterRaster.MAX_SHORE_DISTANCE) + 1; int shelf = shore.shelf() + 1; @@ -192,6 +206,39 @@ final class ChunkFeatures { lakeReach.collect(mask, minX - shoreReach, minZ - shoreReach, maxX + shoreReach, maxZ + shoreReach); System.arraycopy(grownBox(mask), 0, lakeBoxes, 4 * index, 4); } + inert = segments.isEmpty() && !inletsTouch(minX - 1, minZ - 1, maxX + 1, maxZ + 1) + && !pondsTouch(minX - 1 - reach, minZ - 1 - reach, maxX + 1 + reach, maxZ + 1 + reach) + && !lakesTouch(minX - 2 - shoreReach, minZ - 2 - shoreReach, maxX + 2 + shoreReach, maxZ + 2 + shoreReach); + } + + private boolean inletsTouch(int minX, int minZ, int maxX, int maxZ) { + for (int box = 0; box < inletBoxes.length; box += 4) { + if (inletBoxes[box] <= maxX && inletBoxes[box + 2] >= minX && inletBoxes[box + 1] <= maxZ && inletBoxes[box + 3] >= minZ) { + return true; + } + } + return false; + } + + private boolean pondsTouch(int minX, int minZ, int maxX, int maxZ) { + for (Pond pond : ponds) { + PondMask mask = pond.mask(); + int fromX = mask.originX() - (mask.step() >> 1); + int fromZ = mask.originZ() - (mask.step() >> 1); + if (fromX <= maxX && fromX + mask.width() * mask.step() > minX && fromZ <= maxZ && fromZ + mask.depth() * mask.step() > minZ) { + return true; + } + } + return false; + } + + private boolean lakesTouch(int minX, int minZ, int maxX, int maxZ) { + for (int box = 0; box < lakeBoxes.length; box += 4) { + if (lakeBoxes[box] <= maxX && lakeBoxes[box + 2] > minX && lakeBoxes[box + 1] <= maxZ && lakeBoxes[box + 3] > minZ) { + return true; + } + } + return false; } private static Map windowsByKey(int chunkX, int chunkZ, List windows) { diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkRasterizer.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkRasterizer.java index 369be027d..9a27928ad 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkRasterizer.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ChunkRasterizer.java @@ -37,7 +37,7 @@ public final class ChunkRasterizer { List lakeRegionPlans) { Objects.requireNonNull(naturalHeight, "naturalHeight"); ChunkFeatures features = ChunkFeatures.gather(settings, chunkX, chunkZ, halo, windows, lakeRegionPlans, lakeRegions(chunkX, chunkZ)); - if (features.empty()) { + if (features.inert()) { return ChunkWaterRaster.EMPTY; } return new RasterComposer(settings, features, chunkX, chunkZ).compose(naturalHeight); diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ReachSegments.java b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ReachSegments.java index 7661088e5..60bebde2d 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ReachSegments.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/raster/ReachSegments.java @@ -6,6 +6,8 @@ import java.util.Arrays; final class ReachSegments { private static final int RUN = 16; + private static final int COVER_SHIFT = 2; + private static final float COVER_MARGIN = 1f; private Reach[] reaches; private int[] segments; @@ -24,6 +26,13 @@ final class ReachSegments { private int[] runBands = new int[8]; private int runs; private boolean sealed; + private boolean covered; + private int coverX; + private int coverZ; + private int coverWidth; + private int coverDepth; + private int[] coverStarts; + private int[] coverEntries; ReachSegments(int capacity) { int initial = Math.max(1, capacity); @@ -71,6 +80,19 @@ final class ReachSegments { } } + boolean isEmpty() { + return size == 0; + } + + void coverChannels(int minX, int minZ, int maxX, int maxZ) { + covered = true; + coverX = minX; + coverZ = minZ; + coverWidth = ((maxX - minX) >> COVER_SHIFT) + 1; + coverDepth = ((maxZ - minZ) >> COVER_SHIFT) + 1; + sealed = false; + } + void nearest(double x, double z, Scope scope, ReachCursor cursor) { if (!sealed) { seal(); @@ -89,6 +111,16 @@ final class ReachSegments { hint(channelHint, x, z, unbounded, cursor); hint(bankHint, x, z, unbounded, cursor); hint(edgeHint, x, z, unbounded, cursor); + int cell = scope == Scope.CHANNEL ? coveredCell(x, z) : -1; + if (cell >= 0) { + for (int entry = coverStarts[cell]; entry < coverStarts[cell + 1]; entry++) { + int index = coverEntries[entry]; + if (index != channelHint && index != bankHint && index != edgeHint && !beyondSegment(index, x, z, scope, cursor)) { + evaluate(index, x, z, false, cursor); + } + } + return; + } int first = 0; for (int run = 0; run < runs; run++) { int end = runEnds[run]; @@ -99,9 +131,7 @@ final class ReachSegments { continue; } for (int index = first; index < end; index++) { - if (!beyond(x, z, Math.min(startX[index], endX[index]), Math.min(startZ[index], endZ[index]), Math.max(startX[index], endX[index]), - Math.max(startZ[index], endZ[index]), limit(radius[index], bands[index], scope, cursor)) - && index != channelHint && index != bankHint && index != edgeHint) { + if (!beyondSegment(index, x, z, scope, cursor) && index != channelHint && index != bankHint && index != edgeHint) { evaluate(index, x, z, unbounded, cursor); } } @@ -109,6 +139,23 @@ final class ReachSegments { } } + private int coveredCell(double x, double z) { + if (coverStarts == null) { + return -1; + } + int cellX = ((int) Math.floor(x) - coverX) >> COVER_SHIFT; + int cellZ = ((int) Math.floor(z) - coverZ) >> COVER_SHIFT; + if (cellX < 0 || cellZ < 0 || cellX >= coverWidth || cellZ >= coverDepth) { + return -1; + } + return cellZ * coverWidth + cellX; + } + + private boolean beyondSegment(int index, double x, double z, Scope scope, ReachCursor cursor) { + return beyond(x, z, Math.min(startX[index], endX[index]), Math.min(startZ[index], endZ[index]), Math.max(startX[index], endX[index]), + Math.max(startZ[index], endZ[index]), limit(radius[index], bands[index], scope, cursor)); + } + private void hint(int index, double x, double z, boolean unbounded, ReachCursor cursor) { if (index >= 0 && index < size) { evaluate(index, x, z, unbounded, cursor); @@ -171,9 +218,56 @@ final class ReachSegments { addRun(start, end); start = end; } + if (covered) { + cover(); + } sealed = true; } + private void cover() { + int cells = coverWidth * coverDepth; + int[] starts = new int[cells + 1]; + int[] range = new int[4]; + for (int index = 0; index < size; index++) { + if (coverRange(index, range)) { + for (int cellZ = range[1]; cellZ <= range[3]; cellZ++) { + for (int cellX = range[0]; cellX <= range[2]; cellX++) { + starts[cellZ * coverWidth + cellX + 1]++; + } + } + } + } + for (int cell = 0; cell < cells; cell++) { + starts[cell + 1] += starts[cell]; + } + int[] entries = new int[starts[cells]]; + int[] fill = Arrays.copyOf(starts, cells); + for (int index = 0; index < size; index++) { + if (coverRange(index, range)) { + for (int cellZ = range[1]; cellZ <= range[3]; cellZ++) { + for (int cellX = range[0]; cellX <= range[2]; cellX++) { + entries[fill[cellZ * coverWidth + cellX]++] = index; + } + } + } + } + coverStarts = starts; + coverEntries = entries; + } + + private boolean coverRange(int index, int[] range) { + float reach = radius[index] + COVER_MARGIN; + range[0] = Math.max(0, coverCell(Math.min(startX[index], endX[index]) - reach, coverX)); + range[1] = Math.max(0, coverCell(Math.min(startZ[index], endZ[index]) - reach, coverZ)); + range[2] = Math.min(coverWidth - 1, coverCell(Math.max(startX[index], endX[index]) + reach, coverX)); + range[3] = Math.min(coverDepth - 1, coverCell(Math.max(startZ[index], endZ[index]) + reach, coverZ)); + return range[0] <= range[2] && range[1] <= range[3]; + } + + private static int coverCell(float block, int origin) { + return ((int) Math.floor(block) - origin) >> COVER_SHIFT; + } + private void addRun(int start, int end) { if (runs == runEnds.length) { runEnds = Arrays.copyOf(runEnds, 2 * runs); diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/raster/ReachSegmentsTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/raster/ReachSegmentsTest.java new file mode 100644 index 000000000..299c4178d --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/raster/ReachSegmentsTest.java @@ -0,0 +1,95 @@ +package art.arcane.iris.generation.hydrology.raster; + +import art.arcane.iris.generation.hydrology.config.IrisBankStyle; +import art.arcane.iris.generation.hydrology.config.IrisHydrologyMaterials; +import art.arcane.iris.generation.hydrology.water.HydrologyTier; +import art.arcane.iris.generation.hydrology.water.Reach; +import art.arcane.iris.generation.hydrology.water.ReachStyle; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.SplittableRandom; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +public class ReachSegmentsTest { + private static final HydrologyTier TIER = new HydrologyTier("test", 0f, 1f, 4f, 1f, 0.4f, 1, true, 6, 4, IrisBankStyle.SMOOTH, + false, new IrisHydrologyMaterials(), "water", List.of()); + + @Test + public void coveredChannelLookupsMatchTheFullScan() { + SplittableRandom random = new SplittableRandom(1337L); + int hits = 0; + for (int trial = 0; trial < 40; trial++) { + int originX = random.nextInt(-2000, 2000) << 4; + int originZ = random.nextInt(-2000, 2000) << 4; + List reaches = new ArrayList<>(); + int count = 1 + random.nextInt(12); + for (int walk = 0; walk < count; walk++) { + reaches.add(randomWalk(trial * 100L + walk, originX + 8, originZ + 8, random)); + } + ReachSegments scanned = segments(reaches, originX, originZ); + ReachSegments covered = segments(reaches, originX, originZ); + covered.coverChannels(originX - 17, originZ - 17, originX + 32, originZ + 32); + ReachCursor scan = new ReachCursor(); + ReachCursor cover = new ReachCursor(); + for (int z = originZ - 20; z <= originZ + 35; z++) { + for (int x = originX - 20; x <= originX + 35; x++) { + ReachSegments.Scope scope = random.nextInt(4) == 0 ? ReachSegments.Scope.SHORE : ReachSegments.Scope.CHANNEL; + scanned.nearest(x, z, scope, scan); + covered.nearest(x, z, scope, cover); + String at = scope + " at " + x + "," + z; + assertSameChoice(at + " channel", scan.channel, cover.channel); + assertSameChoice(at + " bank", scan.bank, cover.bank); + assertSameChoice(at + " edge", scan.edge, cover.edge); + if (scan.channel.reach != null) { + hits++; + } + } + } + } + assertTrue("random walks cross the covered area, hits=" + hits, hits > 1000); + } + + private static void assertSameChoice(String what, ReachCursor.Choice expected, ReachCursor.Choice actual) { + assertSame(what, expected.reach, actual.reach); + assertEquals(what, expected.segment, actual.segment); + assertEquals(what, expected.index, actual.index); + assertEquals(what, Double.doubleToLongBits(expected.t), Double.doubleToLongBits(actual.t)); + assertEquals(what, Double.doubleToLongBits(expected.distance), Double.doubleToLongBits(actual.distance)); + } + + private static ReachSegments segments(List reaches, int originX, int originZ) { + ReachSegments segments = new ReachSegments(16); + for (Reach reach : reaches) { + segments.addNear(reach, originX - 1f, originZ - 1f, originX + 16f, originZ + 16f, ChunkWaterRaster.MAX_SHORE_DISTANCE + 1f); + } + return segments; + } + + private static Reach randomWalk(long id, int centerX, int centerZ, SplittableRandom random) { + int points = 2 + random.nextInt(30); + float[] xs = new float[points]; + float[] zs = new float[points]; + float[] halfWidth = new float[points]; + xs[0] = centerX + (float) random.nextDouble(-60D, 60D); + zs[0] = centerZ + (float) random.nextDouble(-60D, 60D); + halfWidth[0] = (float) random.nextDouble(0.5D, 9D); + for (int point = 1; point < points; point++) { + xs[point] = xs[point - 1] + (float) random.nextDouble(-12D, 12D); + zs[point] = zs[point - 1] + (float) random.nextDouble(-12D, 12D); + halfWidth[point] = random.nextInt(8) == 0 ? halfWidth[point - 1] : (float) random.nextDouble(0.5D, 9D); + } + int[] grade = new int[points]; + float[] depth = new float[points]; + ReachStyle[] style = new ReachStyle[points]; + Arrays.fill(grade, 64); + Arrays.fill(depth, 1f); + Arrays.fill(style, ReachStyle.RUN); + return new Reach(id, TIER, -1L, xs, zs, grade, halfWidth, depth, style, 100f); + } +}