diff --git a/core/src/main/java/art/arcane/iris/generation/bake/TerrainProgram.java b/core/src/main/java/art/arcane/iris/generation/bake/TerrainProgram.java index b631c660c..a67f63719 100644 --- a/core/src/main/java/art/arcane/iris/generation/bake/TerrainProgram.java +++ b/core/src/main/java/art/arcane/iris/generation/bake/TerrainProgram.java @@ -34,6 +34,41 @@ public final class TerrainProgram { return new TerrainProgram(generators, programs, heightSeed); } + public long heightSeed() { + return heightSeed; + } + + public double average(int group, TerrainBatchScratch scratch, double low, double high, double x, double z) { + IrisGenerator[] members = generators[group]; + if (members.length == 0) { + return 0D; + } + if (!Double.isFinite(low) || !Double.isFinite(high)) { + throw new IllegalStateException("Terrain generator bounds were not finite at " + x + "," + z + + " (generator=" + members[0].getLoadKey() + ", bounds=" + low + ".." + high + ")."); + } + double height = 0D; + if (low == high) { + for (int member = 0; member < members.length; member++) { + height += low; + } + return height / members.length; + } + GeneratorEvaluations.scalar(members.length); + GeneratorProgram[] compiled = programs[group]; + for (int member = 0; member < members.length; member++) { + GeneratorProgram program = compiled[member]; + double noise = program != null ? program.height(scratch, x, z) : members[member].getHeight(x, z, heightSeed); + if (!Double.isFinite(noise)) { + throw new IllegalStateException("Terrain generator noise was not finite at " + x + "," + z + + " (generator=" + members[member].getLoadKey() + ", noise=" + noise + + ", bounds=" + low + ".." + high + ")."); + } + height += M.lerp(low, high, noise); + } + return height / members.length; + } + public int groups() { return generators.length; } diff --git a/core/src/main/java/art/arcane/iris/generation/noise/GeneratorProgram.java b/core/src/main/java/art/arcane/iris/generation/noise/GeneratorProgram.java index 233a7fd3a..360746480 100644 --- a/core/src/main/java/art/arcane/iris/generation/noise/GeneratorProgram.java +++ b/core/src/main/java/art/arcane/iris/generation/noise/GeneratorProgram.java @@ -58,6 +58,70 @@ public final class GeneratorProgram { surfaceHeights(scratch, xs, zs, count, out); } + public double height(TerrainBatchScratch scratch, double x, double z) { + if (layers.length == 0) { + return 0D; + } + if (surfaceDetail == 1D) { + return sample(scratch, x, z); + } + double lowerX = Math.floor(x / SURFACE_GRID) * SURFACE_GRID; + double lowerZ = Math.floor(z / SURFACE_GRID) * SURFACE_GRID; + double dx = (x - lowerX) / SURFACE_GRID; + double dz = (z - lowerZ) / SURFACE_GRID; + IrisGenerator.SurfaceCacheStripe[] stripes = generator.surfaceStripes(); + double northWest = lattice(scratch, stripes, lowerX, lowerZ); + if (dx == 0D && dz == 0D) { + return northWest; + } + double north = dx == 0D ? northWest + : IrisInterpolation.lerp(northWest, lattice(scratch, stripes, lowerX + SURFACE_GRID, lowerZ), dx); + double smooth = north; + if (dz != 0D) { + double southWest = lattice(scratch, stripes, lowerX, lowerZ + SURFACE_GRID); + double south = dx == 0D ? southWest + : IrisInterpolation.lerp(southWest, lattice(scratch, stripes, lowerX + SURFACE_GRID, lowerZ + SURFACE_GRID), dx); + smooth = IrisInterpolation.lerp(north, south, dz); + } + return surfaceDetail == 0D ? smooth : smooth + (sample(scratch, x, z) - smooth) * surfaceDetail; + } + + private double lattice(TerrainBatchScratch scratch, IrisGenerator.SurfaceCacheStripe[] stripes, double x, double z) { + if (x != (int) x || z != (int) z) { + return sample(scratch, x, z); + } + int mark = scratch.mark(); + double[] cached = scratch.buffer(1); + boolean hit = IrisGenerator.cachedSurface(stripes, x, z, superSeed, cached, 0); + double value = cached[0]; + scratch.reset(mark); + if (hit) { + return value; + } + double height = sample(scratch, x, z); + IrisGenerator.storeSurface(stripes, x, z, superSeed, height); + return height; + } + + private double sample(TerrainBatchScratch scratch, double x, double z) { + double sampleX = (x + offsetX) / zoom; + double sampleZ = (z + offsetZ) / zoom; + double value = multiplicitive ? 1D : 0D; + for (NoiseLayerProgram layer : layers) { + double noise = layer.sample(scratch, sampleX, sampleZ); + value = multiplicitive ? value * noise : value + noise; + } + value = multiplicitive ? value * opacity : (value / totalOpacity) * opacity; + if (Double.isNaN(value)) { + value = 0D; + } + if (cliffs != null) { + double height = IrisInterpolation.lerp(cliffHeightMin, cliffHeightMax, cliffs.sample(scratch, sampleX, sampleZ)); + value = (Math.round((value * 255D) / height) * height) / 255D; + } + return cellCracks ? generator.cell(x, z, value, 0D) : value; + } + private void surfaceHeights(TerrainBatchScratch scratch, double[] xs, double[] zs, int count, double[] out) { int mark = scratch.mark(); int indexMark = scratch.indexMark(); diff --git a/core/src/main/java/art/arcane/iris/generation/noise/NoiseLayerProgram.java b/core/src/main/java/art/arcane/iris/generation/noise/NoiseLayerProgram.java index 06c0571a4..e9bd1df41 100644 --- a/core/src/main/java/art/arcane/iris/generation/noise/NoiseLayerProgram.java +++ b/core/src/main/java/art/arcane/iris/generation/noise/NoiseLayerProgram.java @@ -125,6 +125,53 @@ public final class NoiseLayerProgram { scratch.reset(mark); } + public double sample(TerrainBatchScratch scratch, double x, double z) { + if (!enabled) { + return offsetY; + } + double sampleX = x; + double sampleZ = z; + if (children.length > 0) { + int mark = scratch.mark(); + double[] pair = scratch.buffer(2); + for (int child = 0; child < children.length; child++) { + children[child].samplePair(scratch, x, z, pair); + double offset = childOffsets[child]; + sampleX += pair[0] - offset; + sampleZ += pair[1] - offset; + } + scratch.reset(mark); + } + return post(program.sample2D((sampleX / zoom) + offsetX, (sampleZ / zoom) + offsetZ)); + } + + private void samplePair(TerrainBatchScratch scratch, double x, double z, double[] out) { + if (!enabled) { + out[0] = offsetY; + out[1] = offsetY; + return; + } + double forwardX = x; + double forwardZ = z; + double reverseX = z; + double reverseZ = x; + if (children.length > 0) { + int mark = scratch.mark(); + double[] pair = scratch.buffer(2); + for (int child = 0; child < children.length; child++) { + children[child].samplePair(scratch, x, z, pair); + double offset = childOffsets[child]; + forwardX += pair[0] - offset; + forwardZ += pair[1] - offset; + reverseX += pair[1] - offset; + reverseZ += pair[0] - offset; + } + scratch.reset(mark); + } + out[0] = post(program.sample2D((forwardX / zoom) + offsetX, (forwardZ / zoom) + offsetZ)); + out[1] = post(program.sample2D((reverseX / zoom) + offsetX, (reverseZ / zoom) + offsetZ)); + } + private double post(double raw) { double n = raw * opacity; n = negative ? (-n + opacity) : n; 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 db6dface3..61c918c25 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 @@ -2149,7 +2149,19 @@ public class IrisComplex implements DataProvider { double hi = sampledBounds.max(); double lo = sampledBounds.min(); - return averageGeneratorHeights(generators, lo, hi, x, z, seed + 239945); + return groupHeight(interpolatorIndex, generators, lo, hi, x, z, seed + 239945); + } + + private double groupHeight(int group, IrisGenerator[] generators, double low, double high, double x, double z, long heightSeed) { + TerrainProgram program = terrainProgram; + if (program == null && terrainEngine != null) { + program = terrainProgram(); + } + if (program == null || program.heightSeed() != heightSeed || group >= program.groups() + || program.groupSize(group) != generators.length || generators != frozenGenerators[group]) { + return averageGeneratorHeights(generators, low, high, x, z, heightSeed); + } + return program.average(group, BATCH_SCRATCH.get(), low, high, x, z); } private NoiseBounds gridSampleBounds(Engine engine, IrisInterpolator interpolator, int interpolatorIndex, IrisGenerator[] generators, double x, double z) { @@ -2349,7 +2361,7 @@ public class IrisComplex implements DataProvider { lo = biLerp(boundsLow(b00), boundsLow(b10), boundsLow(b01), boundsLow(b11), fx, fz); hi = biLerp(boundsHigh(b00), boundsHigh(b10), boundsHigh(b01), boundsHigh(b11), fx, fz); } - h += averageGeneratorHeights(generators, lo, hi, x, z, heightSeed); + h += groupHeight(interpolatorIndex, generators, lo, hi, x, z, heightSeed); } return h; diff --git a/core/src/test/java/art/arcane/iris/generation/noise/GeneratorProgramParityTest.java b/core/src/test/java/art/arcane/iris/generation/noise/GeneratorProgramParityTest.java index bb2df6c93..016d1c6cb 100644 --- a/core/src/test/java/art/arcane/iris/generation/noise/GeneratorProgramParityTest.java +++ b/core/src/test/java/art/arcane/iris/generation/noise/GeneratorProgramParityTest.java @@ -39,6 +39,26 @@ public class GeneratorProgramParityTest { } } + @Test + public void singlePointProgramHeightsMatchScalarHeightsBitForBit() { + SplittableRandom random = new SplittableRandom(0xFEEDL); + TerrainBatchScratch scratch = new TerrainBatchScratch(); + for (int generatorIndex = 0; generatorIndex < 160; generatorIndex++) { + long shapeSeed = random.nextLong(); + IrisGenerator scalar = generator(new SplittableRandom(shapeSeed)); + IrisGenerator compiled = generator(new SplittableRandom(shapeSeed)); + long superSeed = random.nextLong(); + GeneratorProgram program = compiled.compileProgram(superSeed); + for (int sample = 0; sample < 400; sample++) { + double x = coordinate(random); + double z = random.nextInt(5) == 0 ? x : coordinate(random); + assertEquals("generator " + generatorIndex + " point " + x + "," + z, + Double.doubleToRawLongBits(scalar.getHeight(x, z, superSeed)), + Double.doubleToRawLongBits(program.height(scratch, x, z))); + } + } + } + @Test public void chunkGridsMatchScalarHeightsBitForBit() { SplittableRandom random = new SplittableRandom(0xC0FFEEL);