diff --git a/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java b/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java index fa4187e59..568c92088 100644 --- a/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java +++ b/core/src/main/java/art/arcane/iris/generation/runtime/Engine.java @@ -77,7 +77,6 @@ import java.awt.Color; import java.nio.file.Path; import java.util.Locale; import java.util.Set; -import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -224,10 +223,6 @@ public interface Engine extends DataProvider, Fallible, BlockUpdater, Renderer, EngineWorldManager getWorldManager(); - default UUID getBiomeID(int x, int z) { - return getComplex().getBaseBiomeIDStream().get(x, z); - } - int getParallelism(); void setParallelism(int parallelism); 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 64dcf075c..3632864ee 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 @@ -29,7 +29,6 @@ import art.arcane.iris.generation.subterrain.SubterrainRasterizer; import art.arcane.iris.configuration.IrisSettings; import art.arcane.iris.pack.loading.IrisData; import art.arcane.iris.pack.loading.IrisRegistrant; -import art.arcane.iris.generation.cache.Cache; import art.arcane.iris.studio.generation.BiomeBuffetLayout; import art.arcane.iris.world.history.GenerationBlend; import art.arcane.iris.world.history.TransitionGenerationPlan; @@ -52,8 +51,6 @@ import art.arcane.iris.generation.terrain.InferredType; import art.arcane.iris.generation.biome.IrisBiome; import art.arcane.iris.generation.biome.IrisBiomeGeneratorLink; import art.arcane.iris.pack.value.IrisRaritySelection; -import art.arcane.iris.generation.decoration.IrisDecorationPart; -import art.arcane.iris.generation.decoration.IrisDecorator; import art.arcane.iris.generation.noise.IrisGenerator; import art.arcane.iris.generation.noise.IrisGeneratorStyle; import art.arcane.iris.generation.terrain.ContinentGenerator; @@ -80,12 +77,12 @@ import art.arcane.iris.generation.terrain.ProceduralTerrainHeightSampler; import art.arcane.iris.generation.terrain.HydrologyBankTerrainRuntime; import art.arcane.iris.spi.IrisPlatforms; import art.arcane.iris.spi.IrisLogging; -import art.arcane.volmlib.nativelib.terrain.NativeBiome; import art.arcane.volmlib.nativelib.terrain.NativeBlockState; import art.arcane.volmlib.util.collection.KList; import art.arcane.iris.generation.block.DataProvider; import art.arcane.volmlib.util.math.M; import art.arcane.volmlib.util.math.RNG; +import art.arcane.volmlib.util.interpolation.IrisInterpolation; import art.arcane.volmlib.util.interpolation.NoiseBounds; import art.arcane.volmlib.util.interpolation.NoiseBoundsProvider; import art.arcane.volmlib.util.noise.CNG; @@ -99,8 +96,10 @@ import lombok.Setter; import lombok.ToString; import java.lang.ref.WeakReference; +import java.util.ArrayDeque; import java.util.Arrays; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.EnumMap; @@ -133,6 +132,8 @@ public class IrisComplex implements DataProvider { private static final double SLOPE_RUN = 3D; /** A sheer coast would let shoreMinimumWidth buy unbounded height, so the shore band stops climbing here. */ private static final double MAX_SHORE_BAND_GRADIENT = 3D; + static final double SHORE_NOISE_LOWEST = -0.5D; + static final double SHORE_NOISE_HIGHEST = 1.5D; private static final Comparator INTERPOLATOR_ORDER = Comparator .comparing((IrisInterpolator interpolator) -> interpolator.getFunction().name()) .thenComparingDouble(IrisInterpolator::getHorizontalScale); @@ -176,7 +177,6 @@ public class IrisComplex implements DataProvider { private ProceduralStream regionStream; private ProceduralStream regionStyleStream; private ProceduralStream regionIdentityStream; - private ProceduralStream regionIDStream; private ProceduralStream bridgeStream; private ProceduralStream landBiomeStream; private ProceduralStream caveBiomeStream; @@ -185,10 +185,8 @@ public class IrisComplex implements DataProvider { private ProceduralStream baseBiomeStream; @Getter(AccessLevel.NONE) private final transient boolean reuseNaturalBaseBiome; - private ProceduralStream baseBiomeIDStream; private ProceduralStream naturalTrueBiomeStream; private ProceduralStream trueBiomeStream; - private ProceduralStream trueBiomeDerivativeStream; private ProceduralStream naturalHeightStream; private ProceduralStream baseTerrainHeightStream; @Getter(AccessLevel.NONE) @@ -230,13 +228,6 @@ public class IrisComplex implements DataProvider { private ProceduralStream riverCarveWeightStream; private ProceduralStream riverWaterSurfaceStream; private ProceduralStream topSurfaceStream; - private ProceduralStream terrainSurfaceDecoration; - private ProceduralStream terrainCeilingDecoration; - private ProceduralStream terrainCaveSurfaceDecoration; - private ProceduralStream terrainCaveCeilingDecoration; - private ProceduralStream seaSurfaceDecoration; - private ProceduralStream seaFloorDecoration; - private ProceduralStream shoreSurfaceDecoration; private ProceduralStream rockStream; private ProceduralStream fluidStream; private Map> hydrologyFluidStreams; @@ -350,7 +341,9 @@ public class IrisComplex implements DataProvider { preparedRegions, (region, inferredType) -> compileInferredBiomeStream(engine, region, inferredType, emptyBiome) ); - GeneratorGroup[] generatorGroups = freezeGeneratorGroups(generators); + GeneratorGroup[] generatorGroups = focusBiome == null && !buffetActive + ? contributingGroups(freezeGeneratorGroups(generators), baseCapableBiomes(preparedRegions)) + : freezeGeneratorGroups(generators); int interpolatorCount = generatorGroups.length; frozenInterpolators = new IrisInterpolator[interpolatorCount]; frozenGenerators = new IrisGenerator[interpolatorCount][]; @@ -384,8 +377,6 @@ public class IrisComplex implements DataProvider { : GenerationStreams.cache2D(RegionSelection.select(new RegionSelection.Options( engine.getDimension(), data, regionStyleStream, imageMapRuntime, null)), "regionStream", engine, cacheSize); - regionIDStream = regionIdentityStream.convertCached((i) -> new UUID(Double.doubleToLongBits(i), - String.valueOf(i * 38445).hashCode() * 3245556666L)); caveBiomeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(regionStream, engine, (c, x, z) -> c.getRegion().get(x, z)) .convert((r) -> createInferredBiomeStream(r, InferredType.CAVE)) .convertAware2D(ProceduralStream::get), "caveBiomeStream", engine, cacheSize); @@ -487,8 +478,6 @@ public class IrisComplex implements DataProvider { shoreSlopeStream = terrain3DEnabled ? baseTerrainHeightStream.slope(3) : naturalSlopeStream; trueBiomeStream = focusedBiomes != null ? GenerationStreams.cache2D(focusedBiomes, "trueBiomeStream-focus", engine, cacheSize) : GenerationStreams.cache2D(heightStream .convertAware2D((terrainHeight, x, z) -> resolveHydrologySurfaceBiome(terrainHeight, x, z)), "trueBiomeStream", engine, cacheSize); - trueBiomeDerivativeStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(trueBiomeStream, engine, (c, x, z) -> c.getBiome().get(x, z)) - .convert((b) -> IrisPlatforms.get().registries().biome(b.getDerivativeKey())), "trueBiomeDerivativeStream", engine, cacheSize); riverDistanceStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyDistance), "riverDistanceStream", engine, cacheSize); riverFlowStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyFlow), "riverFlowStream", engine, cacheSize); riverCarveWeightStream = GenerationStreams.cache2DDouble(ProceduralStream.ofDouble(this::resolveHydrologyCarveWeight), "riverCarveWeightStream", engine, cacheSize); @@ -498,26 +487,6 @@ public class IrisComplex implements DataProvider { riverWaterSurfaceStream.get(x, z) )), "heightFluidStream", engine, cacheSize); maxHeightStream = ProceduralStream.ofDouble((x, z) -> height); - terrainSurfaceDecoration = GenerationStreams.cache2D(GenerationStreams.contextInjecting(trueBiomeStream, engine, (c, x, z) -> c.getBiome().get(x, z)) - .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)), "terrainSurfaceDecoration", engine, cacheSize); - terrainCeilingDecoration = GenerationStreams.cache2D(GenerationStreams.contextInjecting(trueBiomeStream, engine, (c, x, z) -> c.getBiome().get(x, z)) - .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.CEILING)), "terrainCeilingDecoration", engine, cacheSize); - terrainCaveSurfaceDecoration = GenerationStreams.cache2D(GenerationStreams.contextInjecting(caveBiomeStream, engine, (c, x, z) -> c.getCave().get(x, z)) - .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.NONE)), "terrainCaveSurfaceDecoration", engine, cacheSize); - terrainCaveCeilingDecoration = GenerationStreams.cache2D(GenerationStreams.contextInjecting(caveBiomeStream, engine, (c, x, z) -> c.getCave().get(x, z)) - .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.CEILING)), "terrainCaveCeilingDecoration", engine, cacheSize); - shoreSurfaceDecoration = GenerationStreams.cache2D(GenerationStreams.contextInjecting(trueBiomeStream, engine, (c, x, z) -> c.getBiome().get(x, z)) - .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SHORE_LINE)), "shoreSurfaceDecoration", engine, cacheSize); - seaSurfaceDecoration = GenerationStreams.cache2D(GenerationStreams.contextInjecting(trueBiomeStream, engine, (c, x, z) -> c.getBiome().get(x, z)) - .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SEA_SURFACE)), "seaSurfaceDecoration", engine, cacheSize); - seaFloorDecoration = GenerationStreams.cache2D(GenerationStreams.contextInjecting(trueBiomeStream, engine, (c, x, z) -> c.getBiome().get(x, z)) - .convertAware2D((b, xx, zz) -> decorateFor(b, xx, zz, IrisDecorationPart.SEA_FLOOR)), "seaFloorDecoration", engine, cacheSize); - baseBiomeIDStream = GenerationStreams.cache2D(GenerationStreams.contextInjecting(trueBiomeStream, engine, (c, x, z) -> c.getBiome().get(x, z)) - .convertAware2D((b, x, z) -> { - UUID d = regionIDStream.get(x, z); - return new UUID(b.getLoadKey().hashCode() * 818223L, - d.hashCode()); - }), "", engine, cacheSize); //@done TerrainTransformer transformer = engine.getDimension().getTerrainTransform() == null ? null : TerrainTransformRegistry.discover().resolve(engine.getDimension().getTerrainTransform()); @@ -1629,24 +1598,6 @@ public class IrisComplex implements DataProvider { return proceduralTerrainHeight.sample(worldX, worldZ); } - private IrisDecorator decorateFor(IrisBiome b, double x, double z, IrisDecorationPart part) { - RNG rngc = new RNG(Cache.key(((int) x), ((int) z))); - - for (IrisDecorator i : b.getDecorators()) { - if (!i.getPartOf().equals(part)) { - continue; - } - - NativeBlockState block = i.getBlockData(b, rngc, x, z, data); - - if (block != null) { - return i; - } - } - - return null; - } - private IrisBiome resolveHydrologySurfaceBiome(double terrainHeight, double x, double z) { HydrologyColumnSample sample = hydrologySample(x, z); HydrologyColumnLayer layer = sample == null ? null : sample.primarySurfaceLayer().orElse(null); @@ -1730,6 +1681,33 @@ public class IrisComplex implements DataProvider { return bandTop + shoreMinimumWidth * Math.min(shoreSlopes.getDouble(x, z) / SLOPE_RUN, MAX_SHORE_BAND_GRADIENT); } + static boolean aboveShoreBand( + double height, + IrisRegion region, + double x, + double z, + double fluidHeight, + ProceduralStream shoreSlopes + ) { + if (region.getShoreMinimumWidth() <= 0) { + double minimum = region.getShoreHeightMin(); + double maximum = region.getShoreHeightMax(); + double lowestTop = fluidHeight + shoreHeightAt(minimum, maximum, SHORE_NOISE_LOWEST); + double highestTop = fluidHeight + shoreHeightAt(minimum, maximum, SHORE_NOISE_HIGHEST); + if (height > Math.max(lowestTop, highestTop)) { + return true; + } + if (height <= Math.min(lowestTop, highestTop)) { + return false; + } + } + return height > shoreBandTop(height, region, x, z, fluidHeight, shoreSlopes); + } + + private static double shoreHeightAt(double minimum, double maximum, double noise) { + return minimum == maximum ? minimum : IrisInterpolation.lerp(minimum, maximum, noise); + } + static IrisBiome resolveSurfaceBiome( double height, IrisBiome biome, @@ -1745,13 +1723,15 @@ public class IrisComplex implements DataProvider { if (biome == null || region == null) { return biome; } - double shoreTop = shoreBandTop(height, region, x, z, fluidHeight, shoreSlopes); - if (height >= fluidHeight - 1 && height <= shoreTop && !biome.isShore()) { - return shoreBiomes.get(x, z); - } - - if (height > shoreTop && !biome.isLand()) { + if (height >= fluidHeight - 1 && !biome.isShore()) { + if (!aboveShoreBand(height, region, x, z, fluidHeight, shoreSlopes)) { + return shoreBiomes.get(x, z); + } + if (!biome.isLand()) { + return landBiomes.get(x, z); + } + } else if (!biome.isLand() && aboveShoreBand(height, region, x, z, fluidHeight, shoreSlopes)) { return landBiomes.get(x, z); } @@ -2058,6 +2038,48 @@ public class IrisComplex implements DataProvider { return groups; } + static GeneratorGroup[] contributingGroups(GeneratorGroup[] groups, Collection baseBiomes) { + List contributing = new ArrayList<>(groups.length); + for (GeneratorGroup group : groups) { + if (contributesHeight(group.generators(), baseBiomes)) { + contributing.add(group); + } + } + return contributing.toArray(new GeneratorGroup[0]); + } + + private static boolean contributesHeight(IrisGenerator[] generators, Collection baseBiomes) { + for (IrisBiome biome : baseBiomes) { + GeneratorBounds bounds = computeGeneratorBounds(generators, biome); + if (bounds.min != 0D || bounds.max != 0D) { + return true; + } + } + return false; + } + + private Collection baseCapableBiomes(KList regions) { + Set reached = Collections.newSetFromMap(new IdentityHashMap<>()); + ArrayDeque pending = new ArrayDeque<>(); + for (IrisRegion region : regions) { + pending.addAll(loadInferredBiomes(region.getLandBiomes(), InferredType.LAND)); + pending.addAll(loadInferredBiomes(region.getSeaBiomes(), InferredType.SEA)); + } + pending.addAll(imageMapRuntime.getMappedBiomes()); + while (!pending.isEmpty()) { + IrisBiome biome = pending.poll(); + if (!reached.add(biome)) { + continue; + } + for (IrisBiome child : biome.getRealChildren(this)) { + if (child != null) { + pending.add(child); + } + } + } + return reached; + } + static double averageGeneratorHeights( IrisGenerator[] generators, double low, @@ -2103,7 +2125,7 @@ public class IrisComplex implements DataProvider { for (int i = 0; i < frozenInterpolators.length; i++) { IdentityHashMap interpolatorBounds = new IdentityHashMap<>(Math.max(allBiomes.size(), 16)); for (IrisBiome biome : allBiomes) { - interpolatorBounds.put(biome, computeGeneratorBounds(engine, frozenGenerators[i], biome)); + interpolatorBounds.put(biome, computeGeneratorBounds(frozenGenerators[i], biome)); } bounds.put(frozenInterpolators[i], interpolatorBounds); } @@ -2111,7 +2133,7 @@ public class IrisComplex implements DataProvider { return bounds; } - private GeneratorBounds computeGeneratorBounds(Engine engine, IrisGenerator[] generators, IrisBiome biome) { + private static GeneratorBounds computeGeneratorBounds(IrisGenerator[] generators, IrisBiome biome) { double min = 0D; double max = 0D; @@ -2145,7 +2167,7 @@ public class IrisComplex implements DataProvider { return local; } - GeneratorBounds computed = computeGeneratorBounds(engine, generators, biome); + GeneratorBounds computed = computeGeneratorBounds(generators, biome); localBounds.put(biome, computed); return computed; } diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexContributingGroupsTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexContributingGroupsTest.java new file mode 100644 index 000000000..c94268a1f --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexContributingGroupsTest.java @@ -0,0 +1,85 @@ +package art.arcane.iris.generation.runtime; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.biome.IrisBiomeGeneratorLink; +import art.arcane.iris.generation.noise.IrisGenerator; +import art.arcane.iris.generation.noise.IrisInterpolator; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.interpolation.InterpolationMethod; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +public class IrisComplexContributingGroupsTest { + @Test + public void groupsWithZeroBoundsInEveryBaseBiomeAreDropped() { + IrisComplex.GeneratorGroup flat = group(InterpolationMethod.BILINEAR_STARCAST_6, 7D, "default"); + IrisComplex.GeneratorGroup hills = group(InterpolationMethod.BILINEAR_STARCAST_9, 12D, "hills", "cliffs"); + IrisComplex.GeneratorGroup shoreOnly = group(InterpolationMethod.BILINEAR_STARCAST_3, 30D, "ocean"); + IrisBiome plains = biome(link("default", 0, 0), link("hills", -4, 12)); + IrisBiome desert = biome(link("default", 0, 0), link("cliffs", 0, 3)); + + IrisComplex.GeneratorGroup[] contributing = IrisComplex.contributingGroups( + new IrisComplex.GeneratorGroup[]{flat, hills, shoreOnly}, List.of(plains, desert)); + + assertEquals(1, contributing.length); + assertSame(hills, contributing[0]); + } + + @Test + public void aNegativeOnlyOrMaximumOnlyLinkKeepsItsGroup() { + IrisComplex.GeneratorGroup sinks = group(InterpolationMethod.BILINEAR, 16D, "sinks"); + IrisComplex.GeneratorGroup peaks = group(InterpolationMethod.STARCAST_12, 42D, "peaks"); + + IrisComplex.GeneratorGroup[] contributing = IrisComplex.contributingGroups( + new IrisComplex.GeneratorGroup[]{sinks, peaks}, + List.of(biome(link("sinks", -6, 0)), biome(link("peaks", 0, 9)))); + + assertEquals(2, contributing.length); + assertSame(sinks, contributing[0]); + assertSame(peaks, contributing[1]); + } + + @Test + public void linksThatCancelAcrossGeneratorsSumToZeroBoundsAndAreDropped() { + IrisComplex.GeneratorGroup pair = group(InterpolationMethod.BILINEAR_STARCAST_9, 52D, "rise", "fall"); + + IrisComplex.GeneratorGroup[] contributing = IrisComplex.contributingGroups( + new IrisComplex.GeneratorGroup[]{pair}, + List.of(biome(link("rise", -5, 5), link("fall", 5, -5)))); + + assertEquals(0, contributing.length); + } + + @Test + public void noBaseBiomesDropsEveryGroup() { + IrisComplex.GeneratorGroup hills = group(InterpolationMethod.BILINEAR_STARCAST_9, 12D, "hills"); + + assertEquals(0, IrisComplex.contributingGroups(new IrisComplex.GeneratorGroup[]{hills}, List.of()).length); + } + + private static IrisComplex.GeneratorGroup group(InterpolationMethod function, double scale, String... keys) { + IrisInterpolator interpolator = new IrisInterpolator().setFunction(function).setHorizontalScale(scale); + IrisGenerator[] generators = new IrisGenerator[keys.length]; + for (int index = 0; index < keys.length; index++) { + IrisGenerator generator = new IrisGenerator(); + generator.setLoadKey(keys[index]); + generator.setInterpolator(interpolator); + generators[index] = generator; + } + return new IrisComplex.GeneratorGroup(interpolator, generators); + } + + private static IrisBiome biome(IrisBiomeGeneratorLink... links) { + IrisBiome biome = new IrisBiome(); + biome.setGenerators(new KList<>(List.of(links))); + return biome; + } + + private static IrisBiomeGeneratorLink link(String generator, int min, int max) { + return new IrisBiomeGeneratorLink().setGenerator(generator).setMin(min).setMax(max); + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexShoreBandTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexShoreBandTest.java new file mode 100644 index 000000000..fb8c9db89 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexShoreBandTest.java @@ -0,0 +1,207 @@ +package art.arcane.iris.generation.runtime; + +import art.arcane.iris.generation.biome.IrisBiome; +import art.arcane.iris.generation.terrain.IrisRegion; +import art.arcane.volmlib.util.collection.KList; +import art.arcane.volmlib.util.noise.CNG; +import art.arcane.volmlib.util.noise.OffsetNoiseGenerator; +import art.arcane.volmlib.util.noise.SimplexNoise; +import art.arcane.volmlib.util.stream.ProceduralStream; +import org.junit.Test; + +import java.util.SplittableRandom; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.withSettings; + +public class IrisComplexShoreBandTest { + private static final double[] SHORE_EXTREMES = {0D, 1.2D, 3.2D, 2.5D, 7.75D, -1.5D, 4D, 0.0001D, 63.9D}; + + @Test + public void shoreHeightGeneratorIsAnIdentityPostSimplexSignature() { + for (int sample = 0; sample < 8; sample++) { + CNG generator = region(sample).getShoreHeightGenerator(); + assertTrue(generator.getGenerator() instanceof OffsetNoiseGenerator); + assertTrue(((OffsetNoiseGenerator) generator.getGenerator()).getBase() instanceof SimplexNoise); + assertEquals(1D, generator.getOpacity(), 0D); + assertTrue(generator.getChildren() == null || generator.getChildren().isEmpty()); + assertEquals(1D, generator.getPower(), 0D); + assertEquals(0D, generator.getUp(), 0D); + assertEquals(0D, generator.getDown(), 0D); + assertEquals(1D, generator.getPatch(), 0D); + assertNull(generator.getCache()); + assertNull(generator.getCustomGenerator()); + } + } + + @Test + public void simplexAmplitudeBoundFitsInsideTheShoreNoiseRange() { + double kernelPeak = 0D; + for (int step = 0; step <= 2_000_000; step++) { + double radius = Math.sqrt(0.5D) * step / 2_000_000D; + double falloff = 0.5D - radius * radius; + kernelPeak = Math.max(kernelPeak, falloff * falloff * falloff * falloff * Math.sqrt(2D) * radius); + } + assertEquals(256D / 19683D, kernelPeak, 1e-9D); + double amplitude = 50D * 3D * 256D / 19683D; + assertTrue(0.5D + amplitude / 2D < IrisComplex.SHORE_NOISE_HIGHEST); + assertTrue(0.5D - amplitude / 2D > IrisComplex.SHORE_NOISE_LOWEST); + } + + @Test + public void shoreHeightNoiseStaysInsideTheProvenRange() { + SplittableRandom random = new SplittableRandom(1337L); + double lowest = Double.POSITIVE_INFINITY; + double highest = Double.NEGATIVE_INFINITY; + for (int sample = 0; sample < 4; sample++) { + CNG generator = region(sample).getShoreHeightGenerator(); + for (int point = 0; point < 250_000; point++) { + double scale = point % 2 == 0 ? 1D : 64D; + double noise = generator.noise(random.nextDouble(-1e6D, 1e6D) / scale, random.nextDouble(-1e6D, 1e6D) / scale); + lowest = Math.min(lowest, noise); + highest = Math.max(highest, noise); + } + } + assertTrue("lowest " + lowest, lowest >= IrisComplex.SHORE_NOISE_LOWEST); + assertTrue("highest " + highest, highest <= IrisComplex.SHORE_NOISE_HIGHEST); + } + + @Test + public void lazyShoreBandMatchesTheEagerBandTop() { + SplittableRandom random = new SplittableRandom(42L); + ProceduralStream slopes = ProceduralStream.ofDouble((x, z) -> 2D); + for (int sample = 0; sample < 400_000; sample++) { + IrisRegion region = region(random.nextInt(64)) + .setShoreHeightMin(SHORE_EXTREMES[random.nextInt(SHORE_EXTREMES.length)]) + .setShoreHeightMax(SHORE_EXTREMES[random.nextInt(SHORE_EXTREMES.length)]) + .setShoreHeightZoom(random.nextBoolean() ? 3.14D : 0.15D) + .setShoreMinimumWidth(random.nextInt(8) == 0 ? 2D : 0D); + double fluid = random.nextInt(4) == 0 ? 63D : random.nextInt(-64, 200); + double height = random.nextInt(3) == 0 + ? fluid + random.nextDouble(-3D, 9D) + : random.nextInt(-64, 320) + (random.nextBoolean() ? 0D : random.nextDouble()); + double x = random.nextInt(-30_000_000, 30_000_000); + double z = random.nextInt(-30_000_000, 30_000_000); + boolean eager = height > IrisComplex.shoreBandTop(height, region, x, z, fluid, slopes); + assertEquals(eager, IrisComplex.aboveShoreBand(height, region, x, z, fluid, slopes)); + } + } + + @Test + public void lazySurfaceBiomeMatchesTheEagerSelection() { + SplittableRandom random = new SplittableRandom(7L); + IrisBiome land = mock(IrisBiome.class, withSettings().stubOnly()); + IrisBiome sea = mock(IrisBiome.class, withSettings().stubOnly()); + IrisBiome shore = mock(IrisBiome.class, withSettings().stubOnly()); + IrisBiome[] bases = new IrisBiome[8]; + for (int flags = 0; flags < bases.length; flags++) { + IrisBiome base = mock(IrisBiome.class, withSettings().stubOnly()); + doReturn((flags & 1) != 0).when(base).isShore(); + doReturn((flags & 2) != 0).when(base).isLand(); + doReturn((flags & 4) != 0).when(base).isAquatic(); + bases[flags] = base; + } + ProceduralStream lands = constant(land); + ProceduralStream seas = constant(sea); + ProceduralStream shores = constant(shore); + ProceduralStream slopes = ProceduralStream.ofDouble((x, z) -> 4D); + for (int sample = 0; sample < 200_000; sample++) { + IrisRegion region = region(random.nextInt(16)) + .setShoreHeightMin(SHORE_EXTREMES[random.nextInt(SHORE_EXTREMES.length)]) + .setShoreHeightMax(SHORE_EXTREMES[random.nextInt(SHORE_EXTREMES.length)]) + .setShoreMinimumWidth(random.nextInt(8) == 0 ? 3D : 0D); + double fluid = 63D; + double height = random.nextBoolean() ? fluid + random.nextInt(-3, 8) : random.nextInt(-64, 320); + double x = random.nextInt(-100_000, 100_000); + double z = random.nextInt(-100_000, 100_000); + IrisBiome base = bases[random.nextInt(bases.length)]; + IrisBiome expected = eagerSurfaceBiome(height, base, region, x, z, fluid, lands, seas, shores, slopes); + assertSame(expected, IrisComplex.resolveSurfaceBiome(height, base, region, x, z, fluid, lands, seas, shores, slopes)); + } + } + + @Test + public void columnsOutsideTheShoreWindowNeverSampleTheShoreNoise() { + IrisRegion region = mock(IrisRegion.class); + doReturn(1.2D).when(region).getShoreHeightMin(); + doReturn(3.2D).when(region).getShoreHeightMax(); + doReturn(0D).when(region).getShoreMinimumWidth(); + doThrow(new AssertionError("The shore noise was sampled for a column outside the shore window")) + .when(region).getShoreHeight(anyDouble(), anyDouble()); + ProceduralStream slopes = ProceduralStream.ofDouble((x, z) -> 0D); + + assertTrue(IrisComplex.aboveShoreBand(68D, region, 3D, 4D, 63D, slopes)); + assertTrue(IrisComplex.aboveShoreBand(250D, region, 3D, 4D, 63D, slopes)); + assertFalse(IrisComplex.aboveShoreBand(63.2D, region, 3D, 4D, 63D, slopes)); + assertFalse(IrisComplex.aboveShoreBand(-20D, region, 3D, 4D, 63D, slopes)); + } + + @Test + public void equalShoreExtremesNeverSampleTheShoreNoise() { + IrisRegion region = mock(IrisRegion.class); + doReturn(2D).when(region).getShoreHeightMin(); + doReturn(2D).when(region).getShoreHeightMax(); + doReturn(0D).when(region).getShoreMinimumWidth(); + doThrow(new AssertionError("Equal shore extremes need no shore noise")) + .when(region).getShoreHeight(anyDouble(), anyDouble()); + ProceduralStream slopes = ProceduralStream.ofDouble((x, z) -> 0D); + + assertFalse(IrisComplex.aboveShoreBand(65D, region, 3D, 4D, 63D, slopes)); + assertTrue(IrisComplex.aboveShoreBand(65.0001D, region, 3D, 4D, 63D, slopes)); + } + + private static IrisRegion region(int variant) { + IrisRegion region = new IrisRegion(); + region.setName("Shore Region " + variant); + region.setLandBiomeZoom(1D + variant * 0.25D); + KList lands = new KList<>(); + for (int index = 0; index < variant % 5; index++) { + lands.add("land-" + index); + } + region.setLandBiomes(lands); + return region; + } + + private static IrisBiome eagerSurfaceBiome( + double height, + IrisBiome biome, + IrisRegion region, + double x, + double z, + double fluidHeight, + ProceduralStream landBiomes, + ProceduralStream seaBiomes, + ProceduralStream shoreBiomes, + ProceduralStream shoreSlopes + ) { + double shoreTop = IrisComplex.shoreBandTop(height, region, x, z, fluidHeight, shoreSlopes); + if (height >= fluidHeight - 1 && height <= shoreTop && !biome.isShore()) { + return shoreBiomes.get(x, z); + } + if (height > shoreTop && !biome.isLand()) { + return landBiomes.get(x, z); + } + if (height < fluidHeight && !biome.isAquatic()) { + return seaBiomes.get(x, z); + } + if (height == fluidHeight && !biome.isShore()) { + return shoreBiomes.get(x, z); + } + return biome; + } + + private static ProceduralStream constant(IrisBiome biome) { + @SuppressWarnings("unchecked") + ProceduralStream stream = mock(ProceduralStream.class, withSettings().stubOnly()); + doReturn(biome).when(stream).get(anyDouble(), anyDouble()); + return stream; + } +} diff --git a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexSurfaceBiomeTest.java b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexSurfaceBiomeTest.java index 67aeaa2e9..db4e716e8 100644 --- a/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexSurfaceBiomeTest.java +++ b/core/src/test/java/art/arcane/iris/generation/runtime/IrisComplexSurfaceBiomeTest.java @@ -119,6 +119,8 @@ public class IrisComplexSurfaceBiomeTest { doReturn(false).when(base).isShore(); doReturn(false).when(base).isAquatic(); doReturn(3D).when(region).getShoreHeight(12D, 18D); + doReturn(1D).when(region).getShoreHeightMin(); + doReturn(5D).when(region).getShoreHeightMax(); IrisBiome resolved = IrisComplex.resolveSurfaceBiome( 68D, @@ -143,6 +145,8 @@ public class IrisComplexSurfaceBiomeTest { IrisRegion region = mock(IrisRegion.class); doReturn(false).when(base).isShore(); doReturn(3D).when(region).getShoreHeight(12D, 18D); + doReturn(1D).when(region).getShoreHeightMin(); + doReturn(5D).when(region).getShoreHeightMax(); IrisBiome resolved = IrisComplex.resolveSurfaceBiome( 63D, @@ -168,6 +172,8 @@ public class IrisComplexSurfaceBiomeTest { doReturn(false).when(aquatic).isShore(); doReturn(false).when(aquatic).isLand(); doReturn(1D).when(region).getShoreHeight(4D, 7D); + doReturn(0.5D).when(region).getShoreHeightMin(); + doReturn(1.5D).when(region).getShoreHeightMax(); IrisBiome resolved = IrisComplex.resolveSurfaceBiome( 66D, @@ -193,6 +199,8 @@ public class IrisComplexSurfaceBiomeTest { doReturn(false).when(base).isShore(); doReturn(false).when(base).isLand(); doReturn(3D).when(region).getShoreHeight(12D, 18D); + doReturn(1D).when(region).getShoreHeightMin(); + doReturn(5D).when(region).getShoreHeightMax(); doReturn(0D).when(region).getShoreMinimumWidth(); IrisBiome resolved = IrisComplex.resolveSurfaceBiome( @@ -218,6 +226,8 @@ public class IrisComplexSurfaceBiomeTest { IrisRegion region = mock(IrisRegion.class); doReturn(false).when(base).isShore(); doReturn(3D).when(region).getShoreHeight(12D, 18D); + doReturn(1D).when(region).getShoreHeightMin(); + doReturn(5D).when(region).getShoreHeightMax(); doReturn(3D).when(region).getShoreMinimumWidth(); IrisBiome resolved = IrisComplex.resolveSurfaceBiome( @@ -244,6 +254,8 @@ public class IrisComplexSurfaceBiomeTest { doReturn(false).when(base).isShore(); doReturn(false).when(base).isLand(); doReturn(3D).when(region).getShoreHeight(12D, 18D); + doReturn(1D).when(region).getShoreHeightMin(); + doReturn(5D).when(region).getShoreHeightMax(); doReturn(3D).when(region).getShoreMinimumWidth(); IrisBiome resolved = IrisComplex.resolveSurfaceBiome( @@ -272,6 +284,8 @@ public class IrisComplexSurfaceBiomeTest { doReturn(false).when(base).isLand(); doReturn(false).when(base).isAquatic(); doReturn(3D).when(region).getShoreHeight(12D, 18D); + doReturn(1D).when(region).getShoreHeightMin(); + doReturn(5D).when(region).getShoreHeightMax(); doReturn(3D).when(region).getShoreMinimumWidth(); assertSame(sea, IrisComplex.resolveSurfaceBiome( @@ -304,6 +318,8 @@ public class IrisComplexSurfaceBiomeTest { public void shoreBandTopTradesHorizontalWidthForHeightOnSlopes() { IrisRegion region = mock(IrisRegion.class); doReturn(3D).when(region).getShoreHeight(12D, 18D); + doReturn(1D).when(region).getShoreHeightMin(); + doReturn(5D).when(region).getShoreHeightMax(); doReturn(3D).when(region).getShoreMinimumWidth(); assertEquals(66D, IrisComplex.shoreBandTop(67D, region, 12D, 18D, 63D, constantSlope(0D)), 0D);