diff --git a/core/src/main/java/art/arcane/iris/generation/decoration/IrisDepositGenerator.java b/core/src/main/java/art/arcane/iris/generation/decoration/IrisDepositGenerator.java index 80ff26a4f..ffc7dd39b 100644 --- a/core/src/main/java/art/arcane/iris/generation/decoration/IrisDepositGenerator.java +++ b/core/src/main/java/art/arcane/iris/generation/decoration/IrisDepositGenerator.java @@ -304,7 +304,12 @@ public class IrisDepositGenerator { return new ClumpFootprint(cells.extentX * 2 + 1, cells.minY, false); } - private static void vanillaEllipsoidCells(RNG rng, int size, CellSink sink) { + /** + * Emits the clump's cells sphere by sphere in x, y, z order, duplicates included. The squared y and z + * offsets are computed once per sphere row; the sums keep the per-cell evaluation order, so every cell + * test is bit-identical to evaluating {@code nx * nx + ny * ny + nz * nz} in place. + */ + static void vanillaEllipsoidCells(RNG rng, int size, CellSink sink) { float angle = rng.nextFloat() * (float) Math.PI; float reach = size / 8F; double startX = Math.sin(angle) * reach; @@ -347,6 +352,8 @@ public class IrisDepositGenerator { } } + double[] squaredY = new double[16]; + double[] squaredZ = new double[16]; for (int i = 0; i < size; i++) { double radius = nodes[i * 4 + 3]; if (radius < 0D) { @@ -361,21 +368,37 @@ public class IrisDepositGenerator { int maxY = Math.max((int) Math.floor(centerY + radius), minY); int minZ = (int) Math.floor(centerZ - radius); int maxZ = Math.max((int) Math.floor(centerZ + radius), minZ); + int spanY = maxY - minY + 1; + int spanZ = maxZ - minZ + 1; + if (spanY > squaredY.length) { + squaredY = new double[spanY]; + } + if (spanZ > squaredZ.length) { + squaredZ = new double[spanZ]; + } + for (int y = 0; y < spanY; y++) { + double ny = (minY + y + 0.5D - centerY) / radius; + squaredY[y] = ny * ny; + } + for (int z = 0; z < spanZ; z++) { + double nz = (minZ + z + 0.5D - centerZ) / radius; + squaredZ[z] = nz * nz; + } for (int x = minX; x <= maxX; x++) { double nx = (x + 0.5D - centerX) / radius; - if (nx * nx >= 1D) { + double squaredX = nx * nx; + if (squaredX >= 1D) { continue; } - for (int y = minY; y <= maxY; y++) { - double ny = (y + 0.5D - centerY) / radius; - if (nx * nx + ny * ny >= 1D) { + for (int y = 0; y < spanY; y++) { + double squaredXY = squaredX + squaredY[y]; + if (squaredXY >= 1D) { continue; } - for (int z = minZ; z <= maxZ; z++) { - double nz = (z + 0.5D - centerZ) / radius; - if (nx * nx + ny * ny + nz * nz < 1D) { - sink.add(x, y, z); + for (int z = 0; z < spanZ; z++) { + if (squaredXY + squaredZ[z] < 1D) { + sink.add(x, minY + y, minZ + z); } } } @@ -601,7 +624,7 @@ public class IrisDepositGenerator { } @FunctionalInterface - private interface CellSink { + interface CellSink { void add(int x, int y, int z); } diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java index ffccce3ab..8be2cf179 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/HydrologyTileCache.java @@ -537,6 +537,14 @@ public final class HydrologyTileCache implements AutoCloseable { chunkColumns(blockX, blockZ); } + /** + * Every column of the block's chunk, indexed {@code localZ * 16 + localX}; a column without hydrology + * is null. The array is shared with the cache and must not be modified. + */ + public HydrologyColumnSample[] chunkSamples(int blockX, int blockZ) { + return chunkColumns(blockX, blockZ).columns(); + } + /** * Whether the column's chunk can be answered without planning: every tile it composes from is * already planned. A caller that must not wait (the server thread answering a height or biome diff --git a/core/src/main/java/art/arcane/iris/generation/hydrology/runtime/IrisHydrologyRuntime.java b/core/src/main/java/art/arcane/iris/generation/hydrology/runtime/IrisHydrologyRuntime.java index 2cff804f9..789b63b75 100644 --- a/core/src/main/java/art/arcane/iris/generation/hydrology/runtime/IrisHydrologyRuntime.java +++ b/core/src/main/java/art/arcane/iris/generation/hydrology/runtime/IrisHydrologyRuntime.java @@ -195,6 +195,11 @@ public final class IrisHydrologyRuntime implements AutoCloseable { cache.prepareChunkColumns(blockX, blockZ); } + /** The block's chunk columns indexed {@code localZ * 16 + localX}; shared, never modified by callers. */ + public HydrologyColumnSample[] chunkSamples(int blockX, int blockZ) { + return cache.chunkSamples(blockX, blockZ); + } + /** Whether the chunk or one of its eight neighbours holds a planned footprint column. */ public boolean hasColumnsAround(int chunkX, int chunkZ) { return cache.hasColumnsAround(chunkX, chunkZ); diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java index 4c04aa714..286bf2a19 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/IrisComplex.java @@ -1062,6 +1062,31 @@ public class IrisComplex implements DataProvider { return transitionHydrologySample(sample, hydrologyWeight, transitionDisplacement); } + /** + * The raw hydrology columns of the chunk holding the block, for a caller that samples every column of + * one chunk through {@link #sampleHydrologyColumn(HydrologyColumnSample[], int, int)}; null without + * hydrology. + */ + public HydrologyColumnSample[] hydrologyChunkSamples(int x, int z) { + return hydrologyRuntime == null ? null : hydrologyRuntime.chunkSamples(x, z); + } + + /** {@link #sampleHydrologyColumn(double, double)} for a column of the chunk {@code chunkSamples} came from. */ + public HydrologyColumnSample sampleHydrologyColumn(HydrologyColumnSample[] chunkSamples, int x, int z) { + if (chunkSamples == null) { + return null; + } + HydrologyColumnSample sample = chunkSamples[(z & 15) << 4 | (x & 15)]; + if (transitionGenerationPlan == null) { + return sample; + } + double hydrologyWeight = transitionHydrologyWeight(x, z); + if (sample == null) { + return sample; + } + return transitionHydrologySample(sample, hydrologyWeight, transitionDisplacement); + } + private HydrologyColumnSnapshot sampleHydrologySnapshot(int x, int z) { if (hydrologyRuntime == null) { return HydrologyColumnSnapshot.ready(null); diff --git a/core/src/main/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuator.java b/core/src/main/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuator.java index 9e5bf6754..7da845d0d 100644 --- a/core/src/main/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuator.java +++ b/core/src/main/java/art/arcane/iris/generation/stage/IrisTerrainNormalActuator.java @@ -154,6 +154,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator riverWaterSurfaceStream; + private final HydrologyColumnSample[] hydrologySamples; private final boolean bedrockEnabled; private final boolean hideOres; private final boolean transitionDisplaced; @@ -186,6 +187,7 @@ public class IrisTerrainNormalActuator extends EngineAssignedActuator expected = new ArrayList<>(); + List actual = new ArrayList<>(); + referenceCells(expectedRng, size, (x, y, z) -> expected.add(pack(x, y, z))); + IrisDepositGenerator.vanillaEllipsoidCells(actualRng, size, (x, y, z) -> actual.add(pack(x, y, z))); + assertEquals("size " + size + " seed " + seed, expected, actual); + assertEquals("size " + size + " seed " + seed, expectedRng.nextLong(), actualRng.nextLong()); + } + } + } + + private static long pack(int x, int y, int z) { + return ((long) (x & 0x1FFFFF) << 42) | ((long) (y & 0x1FFFFF) << 21) | (z & 0x1FFFFF); + } + + private static void referenceCells(RNG rng, int size, IrisDepositGenerator.CellSink sink) { + float angle = rng.nextFloat() * (float) Math.PI; + float reach = size / 8F; + double startX = Math.sin(angle) * reach; + double endX = -Math.sin(angle) * reach; + double startZ = Math.cos(angle) * reach; + double endZ = -Math.cos(angle) * reach; + double startY = rng.nextInt(3) - 2; + double endY = rng.nextInt(3) - 2; + double[] nodes = new double[size * 4]; + + for (int i = 0; i < size; i++) { + float progress = (float) i / size; + double radiusNoise = rng.nextDouble() * size / 16D; + nodes[i * 4] = startX + (endX - startX) * progress; + nodes[i * 4 + 1] = startY + (endY - startY) * progress; + nodes[i * 4 + 2] = startZ + (endZ - startZ) * progress; + nodes[i * 4 + 3] = ((Math.sin(Math.PI * progress) + 1D) * radiusNoise + 1D) / 2D; + } + + for (int i = 0; i < size - 1; i++) { + if (nodes[i * 4 + 3] <= 0D) { + continue; + } + for (int j = i + 1; j < size; j++) { + if (nodes[j * 4 + 3] <= 0D) { + continue; + } + double dx = nodes[i * 4] - nodes[j * 4]; + double dy = nodes[i * 4 + 1] - nodes[j * 4 + 1]; + double dz = nodes[i * 4 + 2] - nodes[j * 4 + 2]; + double dr = nodes[i * 4 + 3] - nodes[j * 4 + 3]; + if (dr * dr <= dx * dx + dy * dy + dz * dz) { + continue; + } + if (dr > 0D) { + nodes[j * 4 + 3] = -1D; + } else { + nodes[i * 4 + 3] = -1D; + } + } + } + + for (int i = 0; i < size; i++) { + double radius = nodes[i * 4 + 3]; + if (radius < 0D) { + continue; + } + double centerX = nodes[i * 4]; + double centerY = nodes[i * 4 + 1]; + double centerZ = nodes[i * 4 + 2]; + int minX = (int) Math.floor(centerX - radius); + int maxX = Math.max((int) Math.floor(centerX + radius), minX); + int minY = (int) Math.floor(centerY - radius); + int maxY = Math.max((int) Math.floor(centerY + radius), minY); + int minZ = (int) Math.floor(centerZ - radius); + int maxZ = Math.max((int) Math.floor(centerZ + radius), minZ); + + for (int x = minX; x <= maxX; x++) { + double nx = (x + 0.5D - centerX) / radius; + if (nx * nx >= 1D) { + continue; + } + for (int y = minY; y <= maxY; y++) { + double ny = (y + 0.5D - centerY) / radius; + if (nx * nx + ny * ny >= 1D) { + continue; + } + for (int z = minZ; z <= maxZ; z++) { + double nz = (z + 0.5D - centerZ) / radius; + if (nx * nx + ny * ny + nz * nz < 1D) { + sink.add(x, y, z); + } + } + } + } + } + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyTileCacheTest.java b/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyTileCacheTest.java index f9b670632..ff13d8553 100644 --- a/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyTileCacheTest.java +++ b/core/src/test/java/art/arcane/iris/generation/hydrology/HydrologyTileCacheTest.java @@ -1125,6 +1125,34 @@ public class HydrologyTileCacheTest { } } + @Test + public void chunkSamplesMatchPerColumnLookups() { + HydrologyTileCache perColumn = new HydrologyTileCache(new HydrologyPlanner(811L, featureSettings(), this::featureTerrain), 8); + HydrologyTileCache perChunk = new HydrologyTileCache(new HydrologyPlanner(811L, featureSettings(), this::featureTerrain), 8); + HydrologyColumnSample plannedColumn = perColumn.get(new HydrologyTileKey(0, 0)).footprint().columns().values().stream() + .filter(HydrologyColumnSample::present) + .findFirst() + .orElseThrow(); + int centreChunkX = Math.floorDiv(plannedColumn.x(), 16); + int centreChunkZ = Math.floorDiv(plannedColumn.z(), 16); + int present = 0; + for (int chunkZ = centreChunkZ - 2; chunkZ <= centreChunkZ + 2; chunkZ++) { + for (int chunkX = centreChunkX - 2; chunkX <= centreChunkX + 2; chunkX++) { + HydrologyColumnSample[] samples = perChunk.chunkSamples(chunkX * 16 + 5, chunkZ * 16 + 11); + assertEquals(256, samples.length); + for (int localZ = 0; localZ < 16; localZ++) { + for (int localX = 0; localX < 16; localX++) { + HydrologyColumnSample expected = perColumn + .columnAt(chunkX * 16 + localX, chunkZ * 16 + localZ).orElse(null); + assertEquals(expected, samples[localZ * 16 + localX]); + present += expected == null ? 0 : 1; + } + } + } + } + assertTrue(present > 0); + } + @Test public void singleColumnContributionReusesItsImmutableSample() { HydrologyColumnSample sample = compositionSample(90, List.of(compositionLayer(11L, 80)));