Refined existing noise

- Hexagon styles produce coherent solid cells with contained subdivisions
- Sierpinski triangles use equilateral recursive geometry
- Noise styles share a 64-block base scale
- Zoom scales the complete pattern with its distortion
- Octaves preserve the base seed and feature scale
- Coordinate dispatch uses the native dimensional sampler
- Classic kernels retain seed precision and bounded output
- Interpolation preserves fractional coordinates and symmetric curves
- Noise caches respect sampling bounds and generation identity
- Clover sampling reuses vertices and cached trigonometry
- Noise and terrain tests cover corrected sampling behavior
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-05 19:32:13 -04:00
1 parent f2887632fe
commit 50894b90ce
47 files changed
+2603 -1542

No files matched your search

@@ -51,6 +51,7 @@ import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
public class IrisCaveCarver3DNearParityTest {
private static final long CARVE_SEED = -54_777_863_784_492_918L;
private static final int SURFACE_CEILING_FADE_DEPTH = 12;
private static final double SURFACE_CEILING_SOLID_EPSILON = 0.000001D;
@@ -297,11 +298,11 @@ public class IrisCaveCarver3DNearParityTest {
public void warpCacheDoesNotCrossContaminateCarverInstancesOnSameThread() throws Exception {
Engine engine = createEngine(128, 92);
IrisCaveProfile firstProfile = createProfile(true, false);
firstProfile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.12D));
firstProfile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.1875D));
IrisCaveProfile secondProfile = createProfile(true, false);
secondProfile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.CELLULAR).zoomed(0.12D));
secondProfile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.CELLULAR).zoomed(0.1875D));
IrisCaveProfile controlProfile = createProfile(true, false);
controlProfile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.CELLULAR).zoomed(0.12D));
controlProfile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.CELLULAR).zoomed(0.1875D));
IrisCaveCarver3D firstCarver = new IrisCaveCarver3D(engine, firstProfile);
IrisCaveCarver3D secondCarver = new IrisCaveCarver3D(engine, secondProfile);
IrisCaveCarver3D controlCarver = new IrisCaveCarver3D(engine, controlProfile);
@@ -1101,13 +1102,14 @@ public class IrisCaveCarver3DNearParityTest {
Engine engine = mock(Engine.class);
IrisData data = mock(IrisData.class);
IrisDimension dimension = mock(IrisDimension.class);
SeedManager seedManager = new SeedManager(942_337_445L);
SeedManager seedManager = mock(SeedManager.class);
EngineMetrics metrics = new EngineMetrics(16);
IrisWorld world = IrisWorld.builder().minHeight(0).maxHeight(worldHeight).build();
doReturn(data).when(engine).getData();
doReturn(dimension).when(engine).getDimension();
doReturn(seedManager).when(engine).getSeedManager();
doReturn(CARVE_SEED).when(seedManager).getCarve();
doReturn(metrics).when(engine).getMetrics();
doReturn(world).when(engine).getWorld();
doReturn(sampledHeight).when(engine).getHeight(anyInt(), anyInt());
@@ -1124,10 +1126,10 @@ public class IrisCaveCarver3DNearParityTest {
profile.setVerticalRange(new IrisRange(0D, 120D));
profile.setVerticalEdgeFade(14);
profile.setVerticalEdgeFadeStrength(0.21D);
profile.setBaseDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.07D));
profile.setDetailDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.17D));
profile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.12D));
profile.setSurfaceBreakStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.09D));
profile.setBaseDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.109375D));
profile.setDetailDensityStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.265625D));
profile.setWarpStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.1875D));
profile.setSurfaceBreakStyle(new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.140625D));
profile.setBaseWeight(1D);
profile.setDetailWeight(0.48D);
profile.setWarpStrength(warp ? 0.37D : 0D);
@@ -1146,14 +1148,14 @@ public class IrisCaveCarver3DNearParityTest {
if (modules) {
KList<IrisCaveFieldModule> caveModules = new KList<>();
caveModules.add(new IrisCaveFieldModule(
new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.11D),
new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.171875D),
0.23D,
0.04D,
new IrisRange(0D, 72D),
false
));
caveModules.add(new IrisCaveFieldModule(
new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.19D),
new IrisGeneratorStyle(NoiseStyle.SIMPLEX).zoomed(0.296875D),
0.17D,
-0.06D,
new IrisRange(24D, 120D),
@@ -2,6 +2,7 @@ package art.arcane.iris.engine.mantle.components;
import art.arcane.iris.engine.object.IrisStructureCarveShape;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.noise.NoiseType;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
@@ -99,7 +100,7 @@ public class StructureCarveEnvelopeTest {
@Test
public void lobedSideReachWandersInsideTheConfiguredPaddingBand() {
CNG lobe = new CNG(new RNG(1337L), 1D, 1);
CNG lobe = new CNG(new RNG(1337L), NoiseType.CLOVER, 1D, 1);
double lowest = 14D;
double highest = 0D;
@@ -17,10 +17,10 @@ public class IrisMathNoiseHotPathParityTest {
HexRandomSizeNoise randomSize = new HexRandomSizeNoise(12345L);
HexJamesNoise james = new HexJamesNoise(54321L);
assertEquals(0.4670864519829246D, randomSize.noise(12.5D, -8.25D), 0D);
assertEquals(0.4935590260372404D, randomSize.noise(-31.75D, 44.5D, 9.125D), 0D);
assertEquals(0.5185375921658786D, james.noise(12.5D, -8.25D), 0D);
assertEquals(0.4557738681128938D, james.noise(-31.75D, 44.5D, 9.125D), 0D);
assertEquals(0.4924242426867459D, randomSize.noise(12.5D, -8.25D), 0D);
assertEquals(0.4856492100555178D, randomSize.noise(-31.75D, 44.5D, 9.125D), 0D);
assertEquals(0.3769400526313378D, james.noise(12.5D, -8.25D), 0D);
assertEquals(0.5487488096537423D, james.noise(-31.75D, 44.5D, 9.125D), 0D);
}
@Test
@@ -33,17 +33,17 @@ public class IrisMathNoiseHotPathParityTest {
.setStyle(NoiseStyle.PERLIN_IRIS.style())
.setExponent(1.15D);
assertEquals(0.3886291844359823D, noiseGenerator.getNoise(9988L, 31.25D, -17.5D, null), 0D);
assertEquals(0.5084984235498856D, noiseGenerator.getNoise(9988L, 31.25D, -17.5D, null), 0D);
IrisGeneratorStyle style = NoiseStyle.IRIS_DOUBLE.style();
assertEquals(0.8640377573263068D, style.createNoCache(new RNG(112233L), null).noiseFast2D(42.5D, -19.75D), 0D);
assertEquals(0.6224926166486215D, style.createNoCache(new RNG(112233L), null).noiseFast2D(42.5D, -19.75D), 0D);
IrisGenerator generator = new IrisGenerator()
.setZoom(9.5D)
.setOpacity(0.91D)
.setComposite(new KList<IrisNoiseGenerator>().qadd(noiseGenerator));
assertEquals(0.5671980255964687D, generator.getHeight(63.0D, -27.0D, 445566L), 0D);
assertEquals(0.43997118860461754D, generator.getHeight(63.0D, -27.0D, 445566L), 0D);
}
@Test
@@ -16,6 +16,7 @@ import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -78,13 +79,11 @@ public final class IrisNoiseGeneratorCacheTest {
private GeneratorFixture() {
when(data.getEngine()).thenReturn(engine);
when(firstGenerator.oct(OCTAVES)).thenReturn(firstGenerator);
when(secondGenerator.oct(OCTAVES)).thenReturn(secondGenerator);
Map<Long, CNG> generators = Map.of(
11L + GENERATOR_SALT - CONFIGURED_SEED, firstGenerator,
29L + GENERATOR_SALT - CONFIGURED_SEED, secondGenerator
);
when(style.createNoCache(any(RNG.class), same(data))).thenAnswer(invocation -> {
when(style.createForLayer(any(RNG.class), same(data), eq(OCTAVES))).thenAnswer(invocation -> {
RNG rng = invocation.getArgument(0);
return generators.get(rng.getSeed());
});
@@ -0,0 +1,19 @@
package art.arcane.iris.engine.object;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class IrisNoiseGeneratorRemapTest {
@Test
public void parametricCurvePreservesEndpointsAndShapesBothHalves() {
double[][] samples = {{0D, 0D}, {0.25D, 0.1D}, {0.5D, 0.5D}, {0.75D, 0.9D}, {1D, 1D}};
for (double[] sample : samples) {
IrisNoiseGenerator layer = new IrisNoiseGenerator()
.setStyle(NoiseStyle.FLAT.style())
.setOpacity(sample[0])
.setParametric(true);
assertEquals(sample[1], layer.getNoise(75L, 13D, -21D, null), 0.000000000001D);
}
}
}
@@ -0,0 +1,201 @@
package art.arcane.iris.engine.object;
import art.arcane.iris.core.loader.IrisData;
import art.arcane.iris.util.project.noise.CNG;
import art.arcane.iris.util.project.noise.NoiseGenerator;
import art.arcane.iris.util.project.noise.NoiseType;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.File;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
public final class NoiseStyleContractTest {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@Test
public void builtInStylesAreFiniteAndRepeatableAcrossWorldCoordinates() {
double[] coordinates = {-29_999_983.25D, -513D, -64D, -1D, 0D, 0.125D, 17D, 64D, 8192D, 29_999_983.25D};
for (NoiseStyle style : NoiseStyle.values()) {
CNG first = style.create(new RNG(813753L));
CNG second = style.create(new RNG(813753L));
for (double coordinate : coordinates) {
double x = coordinate;
double z = coordinate * -0.713D;
double[] samples = {first.noise(x), first.noise(x, z), first.noise(x, 81.25D, z)};
for (double sample : samples) {
assertTrue(style + " is non-finite at " + coordinate, Double.isFinite(sample));
}
assertEquals(style.name(), samples[0], second.noise(x), 0D);
assertEquals(style.name(), samples[1], second.noise(x, z), 0D);
assertEquals(style.name(), samples[2], second.noise(x, 81.25D, z), 0D);
}
}
}
@Test
public void classicPresetsShareOneBaseScale() {
NoiseStyle[] styles = {NoiseStyle.SIMPLEX, NoiseStyle.PERLIN, NoiseStyle.CUBIC,
NoiseStyle.FRACTAL_CUBIC, NoiseStyle.BIOCTAVE_SIMPLEX, NoiseStyle.VIGOCTAVE_SIMPLEX,
NoiseStyle.BIOCTAVE_FRACTAL_CUBIC, NoiseStyle.QUADOCTAVE_FRACTAL_CUBIC};
for (NoiseStyle style : styles) {
CNG generator = style.create(new RNG(7751L));
NoiseGenerator source = generator.getGenerator();
double scale = 100D / 64D;
assertEquals(style.name(), source.noise(27.5D * scale, -83.25D * scale),
generator.noise(27.5D, -83.25D), 0D);
}
assertEquals(2D / 64D, NoiseType.HEXAGON.getCoordinateScale(), 0D);
assertEquals(8D / 64D, NoiseType.SIERPINSKI_TRIANGLE.getCoordinateScale(), 0D);
}
@Test
public void explicitNativeGeneratorFrequenciesKeepTheirUnits() {
CNG generator = new CNG(new RNG(7231L), NoiseType.SIMPLEX, 1D, 1).scale(0.375D);
assertEquals(generator.getGenerator().noise(28D * 0.375D, -74D * 0.375D),
generator.noise(28D, -74D), 0D);
}
@Test
public void explicitZoomDoublesFeatureSize() {
for (NoiseStyle style : NoiseStyle.values()) {
if (style == NoiseStyle.STATIC) {
continue;
}
CNG normal = style.style().createNoCache(new RNG(45231L), null);
CNG enlarged = style.style().zoomed(2D).createNoCache(new RNG(45231L), null);
assertEquals(style.name(), normal.noise(13.25D, -17.75D),
enlarged.noise(26.5D, -35.5D), 0.000000001D);
assertEquals(style.name(), normal.noise(13.25D), enlarged.noise(26.5D), 0.000000001D);
assertEquals(style.name(), normal.noise(13.25D, 41.125D, -17.75D),
enlarged.noise(26.5D, 82.25D, -35.5D), 0.000000001D);
}
}
@Test
public void defaultSettingsPreserveEveryPreset() {
for (NoiseStyle style : NoiseStyle.values()) {
CNG direct = style.create(new RNG(7253L));
CNG configured = style.style().createNoCache(new RNG(7253L), null);
for (int index = 0; index < 32; index++) {
double x = index * 19.713D - 400D;
double z = index * -31.579D + 100D;
assertEquals(style.name(), direct.noise(x, z), configured.noise(x, z), 0D);
assertEquals(style.name(), direct.noise(x, 32.75D, z), configured.noise(x, 32.75D, z), 0D);
}
}
}
@Test
public void builtInStylesStayInsideTheirUnitRange() {
for (NoiseStyle style : NoiseStyle.values()) {
for (long seed : new long[]{0L, 813753L, Long.MAX_VALUE}) {
CNG generator = style.create(new RNG(seed));
for (int index = 0; index < 256; index++) {
double x = index * 17.12345D - 2000D;
double z = index * -27.98765D + 3000D;
double[] values = {generator.noise(x), generator.noise(x, z), generator.noise(x, 53.75D, z)};
for (double value : values) {
assertTrue(style + " produced " + value, value >= 0D && value <= 1D);
}
}
}
}
}
@Test
public void explicitFractureAndCellularizationZoomWithTheirSource() {
IrisGeneratorStyle[] styles = {
NoiseStyle.SIMPLEX.style().setFracture(NoiseStyle.CLOVER.style().setMultiplier(50D).setZoom(1.5D)),
NoiseStyle.IRIS.style().setCellularFrequency(0.7D).setCellularZoom(1.25D)
};
for (IrisGeneratorStyle style : styles) {
CNG normal = style.createNoCache(new RNG(217L), null);
CNG enlarged = style.setZoom(2D).createNoCache(new RNG(217L), null);
assertEquals(normal.noise(19.25D, -71.75D), enlarged.noise(38.5D, -143.5D), 0.000000001D);
}
}
@Test
public void configuredExponentCompoundsThePresetCurve() {
CNG base = NoiseStyle.VASCULAR_THIN.style().createNoCache(new RNG(8831L), null);
CNG squared = NoiseStyle.VASCULAR_THIN.style().setExponent(2D).createNoCache(new RNG(8831L), null);
double value = base.noise(29.5D, -33.25D);
assertEquals(value * value, squared.noise(29.5D, -33.25D), 0.000000000001D);
}
@Test
public void octavePresetsKeepTheirBaseSeed() {
NoiseStyle[][] families = {
{NoiseStyle.SIMPLEX, NoiseStyle.BIOCTAVE_SIMPLEX},
{NoiseStyle.FRACTAL_FBM_SIMPLEX, NoiseStyle.BIOCTAVE_FRACTAL_FBM_SIMPLEX},
{NoiseStyle.FRACTAL_BILLOW_SIMPLEX, NoiseStyle.BIOCTAVE_FRACTAL_BILLOW_SIMPLEX},
{NoiseStyle.FRACTAL_RM_SIMPLEX, NoiseStyle.BIOCTAVE_FRACTAL_RM_SIMPLEX},
{NoiseStyle.FRACTAL_BILLOW_PERLIN, NoiseStyle.BIOCTAVE_FRACTAL_BILLOW_PERLIN},
{NoiseStyle.FRACTAL_CUBIC, NoiseStyle.BIOCTAVE_FRACTAL_CUBIC}
};
for (NoiseStyle[] family : families) {
CNG configured = family[0].create(new RNG(82731L)).oct(2);
CNG preset = family[1].create(new RNG(82731L));
for (int index = 0; index < 32; index++) {
double x = index * 7.125D;
double z = index * -11.875D;
assertEquals(family[1].name(), configured.noise(x, z), preset.noise(x, z), 0D);
assertEquals(family[1].name(), configured.noise(x, 31.25D, z), preset.noise(x, 31.25D, z), 0D);
}
}
}
@Test
public void layerOctavesPreserveAndMultiplyPresetDetail() {
IrisGeneratorStyle style = NoiseStyle.QUADOCTAVE_SIMPLEX.style();
CNG unchanged = style.createForLayer(new RNG(71L), null, 1);
CNG detailed = style.createForLayer(new RNG(71L), null, 2);
assertEquals(4, unchanged.getOct());
assertEquals(8, detailed.getOct());
assertEquals(16, style.createForLayer(new RNG(71L), null, Integer.MAX_VALUE).getOct());
assertNotEquals(unchanged.noise(21D, -31D), detailed.noise(21D, -31D), 0D);
}
@Test
public void cachedLayersRetainTheirOctavesAcrossReload() throws Exception {
File dataFolder = folder.newFolder("pack");
IrisData data = mock(IrisData.class);
when(data.getDataFolder()).thenReturn(dataFolder);
IrisGeneratorStyle cached = NoiseStyle.BIOCTAVE_SIMPLEX.style().setCacheSize(32);
CNG first = cached.createForLayer(new RNG(921L), data, 3);
CNG reloaded = cached.createForLayer(new RNG(921L), data, 3);
CNG uncached = NoiseStyle.BIOCTAVE_SIMPLEX.style().createForLayer(new RNG(921L), data, 3);
CNG different = cached.createForLayer(new RNG(921L), data, 1);
assertEquals(6, first.getOct());
assertEquals(6, reloaded.getOct());
assertEquals(uncached.noise(4D, 7D), first.noise(4D, 7D), 0.000001D);
assertEquals(first.noise(4D, 7D), reloaded.noise(4D, 7D), 0D);
assertNotEquals(first.noise(4D, 7D), different.noise(4D, 7D), 0D);
File[] caches = new File(dataFolder, ".cache").listFiles((parent, name) -> name.endsWith(".cnm"));
assertEquals(2, caches.length);
}
@Test
public void invalidTransformsFailBeforeSampling() {
double[] invalid = {0D, -1D, Double.NaN, Double.POSITIVE_INFINITY, Double.MIN_VALUE};
for (double value : invalid) {
assertThrows(IllegalArgumentException.class,
() -> NoiseStyle.SIMPLEX.style().setZoom(value).createNoCache(new RNG(1L), null));
assertThrows(IllegalArgumentException.class,
() -> NoiseStyle.SIMPLEX.style().setExponent(value).createNoCache(new RNG(1L), null));
assertThrows(IllegalArgumentException.class,
() -> NoiseStyle.SIMPLEX.style().setCellularFrequency(1D).setCellularZoom(value)
.createNoCache(new RNG(1L), null));
}
}
}
@@ -37,6 +37,26 @@ public class IrisInterpolationBoundsTest {
}
}
@Test
public void fractionalBoundsMatchSeparatePassesAndSampleEachCoordinateOnce() {
for (InterpolationMethod method : InterpolationMethod.values()) {
Map<Coordinate, NoiseBounds> expectedSamples = new HashMap<>();
NoiseBounds expected = separatePasses(method, -31.75D, 29.25D, 32D,
IrisInterpolationBoundsTest::asymmetricBounds, expectedSamples);
Map<Coordinate, Integer> actualSamples = new HashMap<>();
NoiseBounds actual = IrisInterpolation.getNoiseBounds(method, -31.75D, 29.25D, 32D, (x, z) -> {
actualSamples.merge(new Coordinate(x, z), 1, Integer::sum);
return asymmetricBounds(x, z);
});
assertBounds(expected, actual);
assertEquals(expectedSamples.keySet(), actualSamples.keySet());
for (int calls : actualSamples.values()) {
assertEquals(1, calls);
}
}
}
@Test
public void nestedProvidersKeepTheirOwnSamplingSequences() {
for (InterpolationMethod method : InterpolationMethod.values()) {
@@ -95,7 +115,7 @@ public class IrisInterpolationBoundsTest {
assertBounds(expected, actual);
}
private static NoiseBounds separatePasses(InterpolationMethod method, int x, int z, double scale,
private static NoiseBounds separatePasses(InterpolationMethod method, double x, double z, double scale,
NoiseBoundsProvider provider, Map<Coordinate, NoiseBounds> samples) {
double minimum = IrisInterpolation.getNoise(method, x, z, scale,
(sx, sz) -> samples.computeIfAbsent(new Coordinate(sx, sz),
@@ -0,0 +1,168 @@
package art.arcane.iris.util.project.interpolation;
import art.arcane.volmlib.util.function.NoiseProvider;
import org.junit.Test;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class IrisInterpolationSamplingTest {
@Test
public void bicubicDispatchUsesSixteenSamplesAndTheCubicKernel() {
NoiseProvider source = (x, z) -> x * x + z * z * z;
AtomicInteger calls = new AtomicInteger();
NoiseProvider countingSource = (x, z) -> {
calls.incrementAndGet();
return source.noise(x, z);
};
double expected = IrisInterpolation.getBicubicNoise(3, 5, 8D, source);
double bilinear = IrisInterpolation.getBilinearNoise(3, 5, 8D, source);
double actual = IrisInterpolation.getNoise(InterpolationMethod.BICUBIC, 3, 5, 8D, countingSource);
assertEquals(expected, actual, 0D);
assertNotEquals(bilinear, actual, 1.0E-12D);
assertEquals(16, calls.get());
}
@Test
public void bilinearSamplingPreservesFractionalAndNegativeCoordinates() {
NoiseProvider source = (x, z) -> x * 0.001D + z * 0.002D;
for (double x : new double[]{-31.75D, -0.25D, 0D, 0.75D, 31.25D, 29_999_999.25D}) {
for (double z : new double[]{-33.5D, -0.5D, 0.5D, 23.75D}) {
assertEquals(source.noise(x, z),
IrisInterpolation.getNoise(InterpolationMethod.BILINEAR, x, z, 32D, source), 1.0E-11D);
}
}
assertEquals(-1, IrisInterpolation.getRadiusFactor(-0.25D, 2.5D));
assertEquals(0, IrisInterpolation.getRadiusFactor(2.25D, 2.5D));
assertEquals(1, IrisInterpolation.getRadiusFactor(2.75D, 2.5D));
}
@Test
public void starcastPreservesFractionalCentersNearTheWorldBorder() {
double x = 30_000_000.25D;
double z = -29_999_999.75D;
List<Coordinate> samples = new ArrayList<>();
IrisInterpolation.getNoise(InterpolationMethod.STARCAST_3, x, z, 8D, (sampleX, sampleZ) -> {
samples.add(new Coordinate(sampleX, sampleZ));
return 0.5D;
});
assertEquals(3, samples.size());
for (int index = 0; index < samples.size(); index++) {
double angle = index * Math.PI * 2D / 3D;
double dx = 8D * (Math.cos(angle) - Math.sin(angle));
double dz = 8D * (Math.sin(angle) + Math.cos(angle));
assertEquals(x + dx, samples.get(index).x, 1.0E-12D);
assertEquals(z + dz, samples.get(index).z, 1.0E-12D);
}
}
@Test
public void compositeSampleCachesKeepFractionalCentersSeparate() {
NoiseProvider source = (x, z) -> Math.sin(x * 0.013D) + Math.cos(z * 0.021D);
for (double x : new double[]{-31.75D, -0.25D, 0.25D, 31.75D}) {
double z = x + 7.5D;
Map<Coordinate, Integer> samples = new HashMap<>();
double expected = IrisInterpolation.getNoise(InterpolationMethod.STARCAST_12, x, z, 32D,
(sampleX, sampleZ) -> IrisInterpolation.getBilinearNoise(sampleX, sampleZ, 32D, source));
double actual = IrisInterpolation.getNoise(InterpolationMethod.BILINEAR_STARCAST_12,
x, z, 32D, (sampleX, sampleZ) -> {
samples.merge(new Coordinate(sampleX, sampleZ), 1, Integer::sum);
return source.noise(sampleX, sampleZ);
});
assertEquals(expected, actual, 0D);
for (int calls : samples.values()) {
assertEquals(1, calls);
}
}
}
@Test
public void fractionalGridSpacingDoesNotCollapseOrJumpAtCellBoundaries() {
NoiseProvider source = (x, z) -> Math.sin(x * 0.73D) * 0.4D + Math.cos(z * 0.59D) * 0.2D;
for (InterpolationMethod method : InterpolationMethod.values()) {
for (double radius : new double[]{0.125D, 0.375D, 2.5D, 7.75D}) {
for (int cell : new int[]{-7, -1, 0, 1, 7}) {
double boundary = cell * radius;
double center = IrisInterpolation.getNoise(method, boundary, 0.37D, radius, source);
double before = IrisInterpolation.getNoise(method, boundary - 1.0E-8D, 0.37D, radius, source);
double after = IrisInterpolation.getNoise(method, boundary + 1.0E-8D, 0.37D, radius, source);
assertTrue(method.name(), Double.isFinite(center));
assertEquals(method.name(), center, before, 1.0E-6D);
assertEquals(method.name(), center, after, 1.0E-6D);
}
}
}
}
@Test
public void gridSamplesKeepTheConfiguredFractionalSpacing() {
List<Coordinate> samples = new ArrayList<>();
IrisInterpolation.getBilinearNoise(-0.25D, 0.125D, 0.375D, (x, z) -> {
samples.add(new Coordinate(x, z));
return x + z;
});
assertEquals(List.of(new Coordinate(-0.375D, 0D), new Coordinate(0D, 0D),
new Coordinate(-0.375D, 0.375D), new Coordinate(0D, 0.375D)), samples);
}
@Test
public void starcastPreservesDoubleSamplePrecisionAndLinearFields() {
NoiseProvider source = (x, z) -> 0.125D + x * 1.0E-11D - z * 2.0E-11D;
for (InterpolationMethod method : new InterpolationMethod[]{InterpolationMethod.STARCAST_3,
InterpolationMethod.STARCAST_6, InterpolationMethod.STARCAST_9, InterpolationMethod.STARCAST_12}) {
assertEquals(method.name(), source.noise(0.25D, -0.75D),
IrisInterpolation.getNoise(method, 0.25D, -0.75D, 32.00000001D, source), 1.0E-16D);
assertNotEquals(method.name(),
IrisInterpolation.getNoise(method, 0D, 0D, 32D, source),
IrisInterpolation.getNoise(method, 0.25D, -0.75D, 32D, source), 1.0E-13D);
}
}
@Test
public void parametricCurvesHaveSymmetricMidpointsAndMonotonicEndpoints() {
for (double alpha : new double[]{1D, 1.5D, 2D, 4D}) {
assertEquals(0D, IrisInterpolation.parametric(0D, alpha), 0D);
assertEquals(0.5D, IrisInterpolation.parametric(0.5D, alpha), 0D);
assertEquals(1D, IrisInterpolation.parametric(1D, alpha), 0D);
double previous = 0D;
for (int sample = 1; sample <= 100; sample++) {
double t = sample / 100D;
double value = IrisInterpolation.parametric(t, alpha);
assertTrue(value >= previous);
assertTrue(value <= 1D);
assertEquals(value, 1D - IrisInterpolation.parametric(1D - t, alpha), 1.0E-15D);
previous = value;
}
}
}
@Test
public void catmullRomKeepsLinearSlopesAcrossGridBoundaries() {
NoiseProvider source = (x, z) -> x * 0.7D - z * 0.3D;
for (double x : new double[]{-7.75D, -0.25D, 0.25D, 7.75D}) {
assertEquals(source.noise(x, 2.75D),
IrisInterpolation.getNoise(InterpolationMethod.CATMULL_ROM_SPLINE, x, 2.75D, 8D, source),
1.0E-12D);
}
}
private record Coordinate(double x, double z) {
}
}
@@ -0,0 +1,260 @@
package art.arcane.iris.util.project.noise;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertThrows;
public class CNGDimensionDispatchTest {
private static final double EPSILON = 1.0E-12D;
@Rule
public TemporaryFolder temporaryFolder = new TemporaryFolder();
@Test
public void eachEntrypointUsesItsNativeDimensionWithScalingAndOpacity() {
CoordinateNoise source = new CoordinateNoise();
CNG noise = new CNG(new RNG(41L), source, 0.6D, 1).scale(0.375D).bake().scale(1.75D);
double scale = 0.375D * 1.75D;
for (double x : new double[]{-13.25D, 0D, 17.5D}) {
double z = x + 2.75D;
double y = x - 4.125D;
double expected1D = source.noise(x * scale) * 0.6D;
double expected2D = source.noise(x * scale, z * scale) * 0.6D;
double expected3D = source.noise(x * scale, y * scale, z * scale) * 0.6D;
assertEquals(expected1D, noise.noise(x), EPSILON);
assertEquals(expected1D, noise.noiseFast1D(x), EPSILON);
assertEquals(expected1D, noise.noise(new double[]{x}), EPSILON);
assertEquals(expected2D, noise.noise(x, z), EPSILON);
assertEquals(expected2D, noise.noiseFast2D(x, z), EPSILON);
assertEquals(expected2D, noise.noise(new double[]{x, z}), EPSILON);
assertEquals(expected2D * 2D - 1D, noise.noiseFastSigned2D(x, z), EPSILON);
assertEquals(expected3D, noise.noise(x, y, z), EPSILON);
assertEquals(expected3D, noise.noiseFast3D(x, y, z), EPSILON);
assertEquals(expected3D, noise.noise(new double[]{x, y, z}), EPSILON);
assertEquals(expected3D * 2D - 1D, noise.noiseFastSigned3D(x, y, z), EPSILON);
}
}
@Test
public void generatorsWithoutCoordinateScalingKeepTheirNativeInputs() {
CoordinateNoise source = new CoordinateNoise() {
@Override
public boolean isNoScale() {
return true;
}
};
CNG fracture = new CNG(new RNG(2L), new CoordinateNoise(), 1D, 1);
CNG noise = new CNG(new RNG(1L), source, 1D, 1).scale(7D).bake().fractureWith(fracture, 12D);
assertEquals(source.noise(3D), noise.noiseFast1D(3D), EPSILON);
assertEquals(source.noise(3D, 7D), noise.noiseFast2D(3D, 7D), EPSILON);
assertEquals(source.noiseSigned(3D, 7D), noise.noiseFastSigned2D(3D, 7D), EPSILON);
assertEquals(source.noise(3D, 5D, 7D), noise.noiseFast3D(3D, 5D, 7D), EPSILON);
assertEquals(source.noiseSigned(3D, 5D, 7D), noise.noiseFastSigned3D(3D, 5D, 7D), EPSILON);
}
@Test
public void fractureSamplesAndDisplacesTheRequestedDimensions() {
CoordinateNoise source = new CoordinateNoise();
CoordinateNoise fractureSource = new CoordinateNoise();
CNG fracture = new CNG(new RNG(2L), fractureSource, 1D, 1);
CNG noise = new CNG(new RNG(1L), source, 0.7D, 1).scale(0.35D).fractureWith(fracture, 12D);
double x = -13.25D;
double y = 5.5D;
double z = 37.75D;
double displaced1D = x + (fractureSource.noise(x) - 0.5D) * 12D;
double displacedX2D = x + (fractureSource.noise(x, z) - 0.5D) * 12D;
double displacedZ2D = z + (fractureSource.noise(z, x) - 0.5D) * 12D;
double displacedX3D = x + (fractureSource.noise(x, y, z) - 0.5D) * 12D;
double displacedY3D = y + (fractureSource.noise(y, x) - 0.5D) * 12D;
double displacedZ3D = z + (fractureSource.noise(z, x, y) - 0.5D) * 12D;
double expected1D = source.noise(displaced1D * 0.35D) * 0.7D;
double expected2D = source.noise(displacedX2D * 0.35D, displacedZ2D * 0.35D) * 0.7D;
double expected3D = source.noise(displacedX3D * 0.35D, displacedY3D * 0.35D, displacedZ3D * 0.35D) * 0.7D;
assertEquals(expected1D, noise.noiseFast1D(x), EPSILON);
assertEquals(expected1D, noise.noise(new double[]{x}), EPSILON);
assertEquals(expected2D, noise.noiseFast2D(x, z), EPSILON);
assertEquals(expected2D, noise.noise(new double[]{x, z}), EPSILON);
assertEquals(expected2D * 2D - 1D, noise.noiseFastSigned2D(x, z), EPSILON);
assertEquals(expected3D, noise.noiseFast3D(x, y, z), EPSILON);
assertEquals(expected3D, noise.noise(new double[]{x, y, z}), EPSILON);
assertEquals(expected3D * 2D - 1D, noise.noiseFastSigned3D(x, y, z), EPSILON);
}
@Test
public void offsetsPreserveTheNativeCoordinatePlane() {
long seed = 75L;
RNG random = new RNG(seed);
double offsetX = random.nextInt(Short.MIN_VALUE, Short.MAX_VALUE);
double offsetZ = random.nextInt(Short.MIN_VALUE, Short.MAX_VALUE);
CoordinateNoise source = new CoordinateNoise();
CNG noise = new CNG(new RNG(9L), source.offset(seed), 1D, 1);
assertEquals(source.noise(3D + offsetX), noise.noise(3D), EPSILON);
assertEquals(source.noise(3D + offsetX, 7D + offsetZ), noise.noise(3D, 7D), EPSILON);
assertEquals(source.noiseSigned(3D + offsetX, 7D + offsetZ), noise.noiseFastSigned2D(3D, 7D), EPSILON);
assertEquals(source.noise(3D + offsetX, 5D, 7D + offsetZ), noise.noise(3D, 5D, 7D), EPSILON);
}
@Test
public void zoomResizesSharedFracturesAndChildrenOnce() {
CNG shared = new CNG(new RNG(71L), new CoordinateNoise(), 1D, 1).scale(0.7D);
CNG noise = new CNG(new RNG(73L), new CoordinateNoise(), 1D, 1)
.scale(0.8D).fractureWith(shared, 3D).child(shared);
double one = noise.noise(7D);
double two = noise.noise(7D, -11D);
double three = noise.noise(7D, 17D, -11D);
noise.zoom(2D);
assertEquals(one, noise.noise(14D), EPSILON);
assertEquals(two, noise.noise(14D, -22D), EPSILON);
assertEquals(three, noise.noise(14D, 34D, -22D), EPSILON);
assertEquals(two * 2D - 1D, noise.noiseFastSigned2D(14D, -22D), EPSILON);
assertEquals(three * 2D - 1D, noise.noiseFastSigned3D(14D, 34D, -22D), EPSILON);
}
@Test
public void zoomInvalidatesBakedAndSignedSamples() {
CoordinateNoise source = new CoordinateNoise();
CNG noise = new CNG(new RNG(31L), source, 1D, 1);
noise.cached(4, "zoom", temporaryFolder.getRoot(), true);
noise.noiseFastSigned2D(2D, 2D);
noise.zoom(2D);
assertEquals(source.noise(1D, 1D), noise.noise(2D, 2D), EPSILON);
assertEquals(source.noiseSigned(1D, 1D), noise.noiseFastSigned2D(2D, 2D), EPSILON);
for (double factor : new double[]{0D, -1D, Double.NaN, Double.POSITIVE_INFINITY, Double.MIN_VALUE}) {
assertThrows(IllegalArgumentException.class, () -> noise.zoom(factor));
}
}
@Test
public void cachedIntegerAndUncachedFractionalSamplesUseTheSamePlane() {
CoordinateNoise source = new CoordinateNoise();
CNG noise = new CNG(new RNG(15L), source, 1D, 1);
noise.noiseFastSigned2D(1D, 2D);
noise.cached(4, "dimensions", temporaryFolder.getRoot(), true);
double cached = (float) source.noise(1D, 2D);
assertEquals(cached, noise.noise(1D, 2D), 0D);
assertEquals(cached, noise.noiseFast2D(1D, 2D), 0D);
assertEquals(cached, noise.noise(new double[]{1D, 2D}), 0D);
assertEquals(cached * 2D - 1D, noise.noiseFastSigned2D(1D, 2D), 0D);
assertEquals(source.noise(1.25D, 2.5D), noise.noiseFast2D(1.25D, 2.5D), EPSILON);
assertEquals(source.noise(1D), noise.noise(1D), EPSILON);
assertEquals(source.noise(1D, 3D, 2D), noise.noise(1D, 3D, 2D), EPSILON);
noise.scale(0.5D);
assertEquals(source.noise(0.5D, 1D), noise.noiseFast2D(1D, 2D), EPSILON);
assertEquals(source.noiseSigned(0.5D, 1D), noise.noiseFastSigned2D(1D, 2D), EPSILON);
}
@Test
public void cachedNoiseDoesNotMirrorOrRepeatOutsideItsBakedArea() {
CNG uncached = new CNG(new RNG(15L), new CoordinateNoise(), 1D, 1);
CNG cached = new CNG(new RNG(15L), new CoordinateNoise(), 1D, 1);
cached.cached(4, "bounded-dimensions", temporaryFolder.getRoot(), true);
double[] coordinates = {-8D, -4D, -1D, -0.25D, 0D, 0.25D, 3D, 3.75D, 4D, 4.25D, 8D, 29_999_999D};
for (double x : coordinates) {
assertEquals(uncached.noise(x), cached.noise(x), 0D);
assertEquals(uncached.noiseFast1D(x), cached.noiseFast1D(x), 0D);
assertEquals(uncached.noise(new double[]{x}), cached.noise(new double[]{x}), 0D);
for (double z : coordinates) {
double expected = uncached.noise(x, z);
if (x >= 0D && x < 4D && z >= 0D && z < 4D && x == (int) x && z == (int) z) {
expected = (float) expected;
}
assertEquals(expected, cached.noise(x, z), 0D);
assertEquals(expected, cached.noiseFast2D(x, z), 0D);
assertEquals(expected, cached.noise(new double[]{x, z}), 0D);
assertEquals(expected * 2D - 1D, cached.noiseFastSigned2D(x, z), EPSILON);
assertEquals(uncached.noise(x, 5D, z), cached.noise(x, 5D, z), 0D);
assertEquals(uncached.noiseFast3D(x, 5D, z), cached.noiseFast3D(x, 5D, z), 0D);
assertEquals(uncached.noiseFastSigned3D(x, 5D, z), cached.noiseFastSigned3D(x, 5D, z), 0D);
assertEquals(uncached.noise(new double[]{x, 5D, z}), cached.noise(new double[]{x, 5D, z}), 0D);
}
}
}
@Test
public void octaveChangesReachNestedOffsetsAndInvalidateSignedSamples() {
OctaveCoordinateNoise source = new OctaveCoordinateNoise();
OffsetNoiseGenerator offset = source.offset(6L).offset(9L);
CNG noise = new CNG(new RNG(77L), offset, 1D, 3);
assertEquals(3, source.octaves);
double previous2D = noise.noiseFastSigned2D(3D, 7D);
double previous3D = noise.noiseFastSigned3D(3D, 5D, 7D);
noise.oct(5);
assertEquals(5, source.octaves);
assertEquals(offset.noiseSigned(3D, 7D), noise.noiseFastSigned2D(3D, 7D), EPSILON);
assertEquals(offset.noiseSigned(3D, 5D, 7D), noise.noiseFastSigned3D(3D, 5D, 7D), EPSILON);
assertNotEquals(previous2D, noise.noiseFastSigned2D(3D, 7D), EPSILON);
assertNotEquals(previous3D, noise.noiseFastSigned3D(3D, 5D, 7D), EPSILON);
}
@Test
public void transformChangesInvalidateSignedSamples() {
CNG noise = new CNG(new RNG(34L), new CoordinateNoise(), 1D, 1);
double previous2D = noise.noiseFastSigned2D(3D, 7D);
double previous3D = noise.noiseFastSigned3D(3D, 5D, 7D);
noise.up(0.125D);
assertEquals(previous2D + 0.25D, noise.noiseFastSigned2D(3D, 7D), EPSILON);
assertEquals(previous3D + 0.25D, noise.noiseFastSigned3D(3D, 5D, 7D), EPSILON);
}
@Test
public void invalidDimensionCountsFailExplicitly() {
CNG noise = new CNG(new RNG(9L), new CoordinateNoise(), 1D, 1);
assertThrows(IllegalArgumentException.class, () -> noise.noise(new double[0]));
assertThrows(IllegalArgumentException.class, () -> noise.noise(new double[]{1D, 2D, 3D, 4D}));
}
private static class CoordinateNoise implements NoiseGenerator {
@Override
public double noise(double x) {
return 0.125D + x * 0.001D;
}
@Override
public double noise(double x, double z) {
return 0.25D + x * 0.001D + z * 0.002D;
}
@Override
public double noise(double x, double y, double z) {
return 0.5D + x * 0.001D + y * 0.002D + z * 0.003D;
}
}
private static final class OctaveCoordinateNoise extends CoordinateNoise implements OctaveNoise {
private int octaves;
@Override
public double noise(double x, double z) {
return super.noise(x, z) / octaves;
}
@Override
public double noise(double x, double y, double z) {
return super.noise(x, y, z) / octaves;
}
@Override
public void setOctaves(int octaves) {
this.octaves = octaves;
}
}
}
@@ -126,7 +126,7 @@ public class CNGInjectorParityTest {
}
private double legacyCombined1D(CompositeFixture fixture, double x) {
double n = fixture.rootGenerator.noise(x, 0D, 0D);
double n = fixture.rootGenerator.noise(x);
double m = 1D;
double valueA = fixture.childA.noise(x);
@@ -143,7 +143,7 @@ public class CNGInjectorParityTest {
}
private double legacyCombined2D(CompositeFixture fixture, double x, double z) {
double n = fixture.rootGenerator.noise(x, z, 0D);
double n = fixture.rootGenerator.noise(x, z);
double m = 1D;
double valueA = fixture.childA.noise(x, z);
@@ -0,0 +1,57 @@
package art.arcane.iris.util.project.noise;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class CellularNoisePrecisionTest {
@Test
public void distantCellBoundariesRetainSubFloatCoordinatePrecision() {
for (double origin : new double[]{-29_999_000D, 29_998_000D}) {
CellularNoise noise = new CellularNoise(1337L);
for (int dimensions = 1; dimensions <= 3; dimensions++) {
double previousCoordinate = origin;
double previousValue = sample(noise, dimensions, origin);
boolean preciseBoundary = false;
for (int step = 1; step <= 8192; step++) {
double coordinate = origin + step * 0.125D;
double value = sample(noise, dimensions, coordinate);
if (Float.floatToRawIntBits((float) coordinate)
== Float.floatToRawIntBits((float) previousCoordinate)
&& value != previousValue) {
preciseBoundary = true;
break;
}
previousCoordinate = coordinate;
previousValue = value;
}
assertTrue("Missing precise cell boundary at " + origin + " in " + dimensions + "D", preciseBoundary);
}
}
}
@Test
public void distantSamplesAreBoundedAndRepeatableAcrossSeeds() {
for (long seed : new long[]{0L, -1L, Long.MIN_VALUE, Long.MAX_VALUE}) {
CellularNoise first = new CellularNoise(seed);
CellularNoise second = new CellularNoise(seed);
for (int dimensions = 1; dimensions <= 3; dimensions++) {
for (int step = -256; step <= 256; step++) {
double coordinate = Math.copySign(29_999_000D, step) + step * 0.125D;
double value = sample(first, dimensions, coordinate);
assertTrue(Double.isFinite(value) && value >= 0D && value <= 1D);
assertEquals(value, sample(second, dimensions, coordinate), 0D);
}
}
}
}
private static double sample(CellularNoise noise, int dimensions, double coordinate) {
return switch (dimensions) {
case 1 -> noise.noise(coordinate);
case 2 -> noise.noise(coordinate, -29_998_713.375D);
default -> noise.noise(coordinate, 73.375D, 29_998_713.375D);
};
}
}
@@ -0,0 +1,178 @@
package art.arcane.iris.util.project.noise;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class ClassicNoiseQualityTest {
private static final long SEED = 972453L;
@Test
public void directCubicUsesConfiguredSeedInBothPrecisions() {
for (long seed : new long[]{1L, -23L, Long.MAX_VALUE}) {
FastNoiseDouble doubles = new FastNoiseDouble(seed);
doubles.setNoiseType(FastNoiseDouble.NoiseType.Cubic);
FastNoise floats = new FastNoise((int) seed);
floats.SetNoiseType(FastNoise.NoiseType.Cubic);
for (int x = -300; x <= 300; x += 37) {
double sampleX = x + 0.193D;
double sampleZ = x * 0.71D + 43.517D;
assertEquals(doubles.GetNoise(sampleX, sampleZ), doubles.GetCubic(sampleX, sampleZ), 0D);
assertEquals(floats.GetNoise((float) sampleX, (float) sampleZ),
floats.GetCubic((float) sampleX, (float) sampleZ), 0F);
}
}
CubicNoise first = new CubicNoise(SEED);
CubicNoise second = new CubicNoise(SEED + 1);
assertNotEquals(first.noise(37.219D), second.noise(37.219D), 1E-8D);
assertNotEquals(first.noise(37.219D, -91.713D), second.noise(37.219D, -91.713D), 1E-8D);
}
@Test
public void perlinDefaultsToQuinticSmoothing() {
FastNoiseDouble source = new FastNoiseDouble(new RNG(SEED).lmax());
source.setLongerp(FastNoiseDouble.Longerp.Qulongic);
source.setFractalType(FastNoiseDouble.FractalType.Billow);
source.setFractalOctaves(1);
PerlinNoise perlin = new PerlinNoise(SEED);
FractalBillowPerlinNoise billow = new FractalBillowPerlinNoise(SEED);
for (int x = -240; x <= 240; x += 17) {
double sampleX = x + 0.37D;
double sampleZ = x * 0.91D - 8.143D;
assertEquals(source.GetPerlin(sampleX, sampleZ), perlin.noiseSigned(sampleX, sampleZ), 0D);
assertEquals(source.GetPerlinFractal(sampleX, sampleZ) / 2D + 0.5D,
billow.noise(sampleX, sampleZ), 0D);
assertEquals(source.GetPerlin(sampleX, 63.21D, sampleZ),
perlin.noiseSigned(sampleX, 63.21D, sampleZ), 0D);
assertEquals(source.GetPerlinFractal(sampleX, 63.21D, sampleZ) / 2D + 0.5D,
billow.noise(sampleX, 63.21D, sampleZ), 0D);
}
}
@Test
public void perlinSlopesRemainContinuousAcrossLatticeBoundaries() {
PerlinNoise noise = new PerlinNoise(SEED);
double step = 0.001D;
for (int x = -400; x <= 400; x += 100) {
double center = noise.noiseSigned(x, 37.219D, -91.713D);
double leftSlope = (center - noise.noiseSigned(x - step, 37.219D, -91.713D)) / step;
double rightSlope = (noise.noiseSigned(x + step, 37.219D, -91.713D) - center) / step;
assertEquals(leftSlope, rightSlope, 1E-8D);
}
}
@Test
public void ridgedSimplexNormalizesTheActualOctaveRange() {
FractalRigidMultiSimplexNoise ridge = new FractalRigidMultiSimplexNoise(SEED);
FastNoiseDouble source = new FastNoiseDouble(new RNG(SEED).lmax());
source.setFractalType(FastNoiseDouble.FractalType.RigidMulti);
for (int octaves : new int[]{1, 2, 4, 8, 16}) {
ridge.setOctaves(octaves);
source.setFractalOctaves(octaves);
double amplitudeSum = 2D - Math.scalb(1D, 1 - octaves);
assertEquals(0D, ridge.f(1D - amplitudeSum), 1E-15D);
assertEquals(1D, ridge.f(1D), 0D);
for (int x = -400; x <= 400; x += 11) {
double sampleX = x + 0.37D;
double sampleZ = x * 0.91D - 8.143D;
double value = ridge.noise(sampleX, sampleZ);
double raw = source.GetSimplexFractal(sampleX, sampleZ);
assertEquals((raw + amplitudeSum - 1D) / amplitudeSum, value, 1E-15D);
assertTrue(value >= 0D && value <= 1D);
assertEquals(value * 2D - 1D, ridge.noiseSigned(sampleX, sampleZ), 0D);
}
}
ridge.setOctaves(1);
assertEquals(1D - Math.abs(source.GetSimplex(37.219D, -91.713D)),
ridge.noise(37.219D, -91.713D), 0D);
}
@Test
public void classicAndCellularFamiliesRemainBoundedAtWorldEdges() {
NoiseGenerator[] generators = {
new CubicNoise(SEED), new FractalCubicNoise(SEED), new FractalBillowPerlinNoise(SEED),
new FractalBillowSimplexNoise(SEED), new FractalFBMSimplexNoise(SEED),
new FractalRigidMultiSimplexNoise(SEED), new GlobNoise(SEED), new CellHeightNoise(SEED),
new VascularNoise(SEED), new CellularNoise(SEED)
};
for (NoiseGenerator generator : generators) {
for (int octave : new int[]{1, 4, 16}) {
if (generator instanceof OctaveNoise fractal) {
fractal.setOctaves(octave);
}
for (double origin : new double[]{-30_000_000D, 0D, 30_000_000D}) {
for (int i = 0; i < 64; i++) {
double x = origin + i * 0.317D;
double z = -origin - i * 0.913D;
assertBounded(generator.noise(x));
assertBounded(generator.noise(x, z));
assertBounded(generator.noise(x, 71.917D, z));
}
}
}
}
}
@Test
public void cellularHeightAndVascularKeepComplementaryProfiles() {
CellHeightNoise height = new CellHeightNoise(SEED);
VascularNoise vascular = new VascularNoise(SEED);
for (int x = -300; x <= 300; x += 13) {
double sampleX = x + 0.91D;
double sampleZ = x * 0.31D - 7.193D;
assertEquals(1D, height.noise(sampleX, sampleZ) + vascular.noise(sampleX, sampleZ), 1E-15D);
assertEquals(1D, height.noise(sampleX, 87.137D, sampleZ)
+ vascular.noise(sampleX, 87.137D, sampleZ), 1E-15D);
}
}
@Test
public void vascularBrightensWhereCellDistancesConverge() {
FastNoiseDouble distance = new FastNoiseDouble(new RNG(SEED).lmax());
distance.setCellularDistanceFunction(FastNoiseDouble.CellularDistanceFunction.Natural);
distance.setCellularReturnType(FastNoiseDouble.CellularReturnType.Distance2Sub);
CellHeightNoise height = new CellHeightNoise(SEED);
VascularNoise vascular = new VascularNoise(SEED);
int borderSamples = 0;
int interiorSamples = 0;
for (int x = -500; x <= 500; x++) {
double sampleX = x + 0.137D;
double sampleZ = x * 3.17D - 71.913D;
double separation = distance.GetCellular(sampleX, sampleZ) + 1D;
if (separation < 0.02D) {
assertTrue(vascular.noise(sampleX, sampleZ) > 0.99D);
assertTrue(height.noise(sampleX, sampleZ) < 0.01D);
borderSamples++;
} else if (separation > 1D) {
assertTrue(vascular.noise(sampleX, sampleZ) < 0.5D);
assertTrue(height.noise(sampleX, sampleZ) > 0.5D);
interiorSamples++;
}
assertEquals(vascular.noise(sampleX, sampleZ) * 2D - 1D,
vascular.noiseSigned(sampleX, sampleZ), 2E-16D);
}
assertTrue(borderSamples > 0);
assertTrue(interiorSamples > 0);
}
@Test
public void fractalConstructorsStartAtOneOctave() {
NoiseGenerator[] generators = {
new FractalCubicNoise(SEED), new FractalFBMSimplexNoise(SEED),
new FractalBillowPerlinNoise(SEED), new FractalBillowSimplexNoise(SEED),
new FractalRigidMultiSimplexNoise(SEED)
};
for (NoiseGenerator generator : generators) {
double initial = generator.noise(37.219D, -91.713D, 17.731D);
((OctaveNoise) generator).setOctaves(1);
assertEquals(initial, generator.noise(37.219D, -91.713D, 17.731D), 0D);
}
}
private void assertBounded(double value) {
assertTrue(Double.isFinite(value) && value >= 0D && value <= 1D);
}
}
@@ -0,0 +1,46 @@
package art.arcane.iris.util.project.noise;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class CloverNoiseBoundaryTest {
@Test
public void threeDimensionalNoiseRemainsContinuousAcrossZeroZ() {
for (long seed : new long[]{0L, 1337L, Long.MIN_VALUE}) {
CloverNoise noise = new CloverNoise(seed);
for (double x = -3.125D; x < 4D; x += 0.375D) {
double atZero = noise.noise(x, 0.417D, 0D);
assertEquals(atZero, noise.noise(x, 0.417D, -1E-7D), 1E-5D);
assertEquals(atZero, noise.noise(x, 0.417D, 1E-7D), 1E-5D);
}
}
}
@Test(timeout = 5000)
public void fractalIterationsCannotDivideByZeroOrRunUnbounded() {
CloverNoise.Noise2D twoDimensional = new CloverNoise.Noise2D(1337L);
CloverNoise.Noise3D threeDimensional = new CloverNoise.Noise3D(1337L);
assertEquals(twoDimensional.noise(0.25D, -0.75D),
twoDimensional.fractalNoise(0.25D, -0.75D, 0), 0D);
assertEquals(threeDimensional.noise(0.25D, 0.5D, -0.75D),
threeDimensional.fractalNoise(0.25D, 0.5D, -0.75D, Integer.MIN_VALUE), 0D);
double bounded2D = twoDimensional.fractalNoise(0.25D, -0.75D, 16);
double bounded3D = threeDimensional.fractalNoise(0.25D, 0.5D, -0.75D, 16);
assertEquals(bounded2D, twoDimensional.fractalNoise(0.25D, -0.75D, Integer.MAX_VALUE), 0D);
assertEquals(bounded3D, threeDimensional.fractalNoise(0.25D, 0.5D, -0.75D, Integer.MAX_VALUE), 0D);
assertTrue(Double.isFinite(bounded2D));
assertTrue(Double.isFinite(bounded3D));
}
@Test
public void stationaryCurlVectorsNormalizeToZero() {
CloverNoise.Vector2 twoDimensional = new CloverNoise.Vector2().normalize();
CloverNoise.Vector3 threeDimensional = new CloverNoise.Vector3().normalize();
assertEquals(0D, twoDimensional.getX(), 0D);
assertEquals(0D, twoDimensional.getY(), 0D);
assertEquals(0D, threeDimensional.getX(), 0D);
assertEquals(0D, threeDimensional.getY(), 0D);
assertEquals(0D, threeDimensional.getZ(), 0D);
}
}
@@ -2,6 +2,13 @@ package art.arcane.iris.util.project.noise;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
public class CloverNoiseParityTest {
@@ -40,4 +47,59 @@ public class CloverNoiseParityTest {
}
}
}
@Test
public void threeDimensionalCoordinateGridPreservesExactNoiseBits() {
long[] expected = {-4534805072329300766L, 938556028071277557L, -6813256383186365522L,
-4452859942568220233L, 5365194784097108235L};
for (int index = 0; index < SEEDS.length; index++) {
assertEquals("seed=" + SEEDS[index], expected[index], coordinateDigest(new CloverNoise(SEEDS[index]), 0));
}
}
@Test(timeout = 15000)
public void concurrentSamplingKeepsThreadScratchIndependent() throws Exception {
CloverNoise shared = new CloverNoise(1337L);
long[] expected = new long[8];
for (int index = 0; index < expected.length; index++) {
expected[index] = coordinateDigest(shared, index * 991);
}
try (ExecutorService executor = Executors.newFixedThreadPool(4)) {
List<Future<Long>> results = new ArrayList<>(expected.length);
for (int index = 0; index < expected.length; index++) {
int start = index * 991;
results.add(executor.submit(() -> coordinateDigest(shared, start)));
}
for (int index = 0; index < expected.length; index++) {
assertEquals(expected[index], results.get(index).get(5, TimeUnit.SECONDS).longValue());
}
}
}
@Test
public void samplingKeepsCallerVectorsUnchanged() {
CloverNoise.Noise3D noise = new CloverNoise.Noise3D(1337L);
CloverNoise.Vector3 point = new CloverNoise.Vector3(0.25D, 0.5D, -0.75D);
double expected = noise.noise(point);
noise.noise(71D, -31D, 113D);
assertEquals(0.25D, point.getX(), 0D);
assertEquals(0.5D, point.getY(), 0D);
assertEquals(-0.75D, point.getZ(), 0D);
assertEquals(expected, noise.noise(point), 0D);
}
private static long coordinateDigest(CloverNoise noise, int start) {
long digest = 0L;
for (int index = start; index < start + 8192; index++) {
double x = ((index * 1580030173L) % 60000000L - 30000000L) / 64D + 0.371D;
double y = ((index * 91437L) % 384L - 64L) / 64D + 0.231D;
double z = ((index * 59260789L) % 60000000L - 30000000L) / 64D - 0.219D;
digest = Long.rotateLeft(digest, 7) ^ Double.doubleToRawLongBits(noise.noise(x, y, z));
}
return digest;
}
}
@@ -6,6 +6,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class FastNoiseDoubleCellularParityTest {
private static final FastNoiseDouble.CellularDistanceFunction[] DISTANCE_FUNCTIONS = new FastNoiseDouble.CellularDistanceFunction[]{
@@ -29,7 +30,7 @@ public class FastNoiseDoubleCellularParityTest {
private static final double[] CELL_3D_Z = getDeclaredDoubleArray("CELL_3D_Z");
@Test
public void cellular2DTwoEdgeModesMatchLegacyUpdateLogic() throws Exception {
public void cellular2DTwoEdgeModesMatchExpandedReference() throws Exception {
for (long seed = 11L; seed <= 17L; seed++) {
double frequency = 0.037D + (seed * 0.001D);
for (FastNoiseDouble.CellularDistanceFunction distanceFunction : DISTANCE_FUNCTIONS) {
@@ -42,7 +43,7 @@ public class FastNoiseDoubleCellularParityTest {
for (int y = -96; y <= 96; y += 13) {
double sampleX = x + 0.37D;
double sampleY = y - 0.41D;
double expected = legacyGetCellular2D(seed, frequency, distanceFunction, returnType, sampleX, sampleY);
double expected = referenceGetCellular2D(seed, frequency, distanceFunction, returnType, sampleX, sampleY);
double actual = noise.GetCellular(sampleX, sampleY);
assertEquals("2D seed=" + seed + " distance=" + distanceFunction + " return=" + returnType + " x=" + sampleX + " y=" + sampleY, expected, actual, 0D);
}
@@ -53,7 +54,7 @@ public class FastNoiseDoubleCellularParityTest {
}
@Test
public void cellular3DTwoEdgeModesMatchLegacyUpdateLogic() throws Exception {
public void cellular3DTwoEdgeModesMatchExpandedReference() throws Exception {
for (long seed = 31L; seed <= 35L; seed++) {
double frequency = 0.029D + (seed * 0.001D);
for (FastNoiseDouble.CellularDistanceFunction distanceFunction : DISTANCE_FUNCTIONS) {
@@ -68,7 +69,7 @@ public class FastNoiseDoubleCellularParityTest {
double sampleX = x + 0.19D;
double sampleY = y - 0.27D;
double sampleZ = z + 0.43D;
double expected = legacyGetCellular3D(seed, frequency, distanceFunction, returnType, sampleX, sampleY, sampleZ);
double expected = referenceGetCellular3D(seed, frequency, distanceFunction, returnType, sampleX, sampleY, sampleZ);
double actual = noise.GetCellular(sampleX, sampleY, sampleZ);
assertEquals("3D seed=" + seed + " distance=" + distanceFunction + " return=" + returnType + " x=" + sampleX + " y=" + sampleY + " z=" + sampleZ, expected, actual, 0D);
}
@@ -79,7 +80,44 @@ public class FastNoiseDoubleCellularParityTest {
}
}
private double legacyGetCellular2D(long seed, double frequency, FastNoiseDouble.CellularDistanceFunction distanceFunction, FastNoiseDouble.CellularReturnType returnType, double x, double y) throws Exception {
@Test
public void secondNearestCellMayLieOutsideImmediateNeighbors() throws Exception {
long seed2D = 2320148477032134941L;
long seed3D = 8394744594210714849L;
for (FastNoiseDouble.CellularDistanceFunction distanceFunction : DISTANCE_FUNCTIONS) {
for (FastNoiseDouble.CellularReturnType returnType : TWO_EDGE_RETURN_TYPES) {
FastNoiseDouble noise = new FastNoiseDouble(seed2D);
noise.setFrequency(1D);
noise.setCellularDistanceFunction(distanceFunction);
noise.setCellularReturnType(returnType);
double x = 783.44443982D;
double y = 1896.50036984D;
assertEquals(referenceGetCellular2D(seed2D, 1D, distanceFunction, returnType, x, y),
noise.GetCellular(x, y), 0D);
noise.setSeed(seed3D);
x = -2741.75935805D;
y = -1049.4161945D;
double z = -1528.00711843D;
assertEquals(referenceGetCellular3D(seed3D, 1D, distanceFunction, returnType, x, y, z),
noise.GetCellular(x, y, z), 0D);
}
}
}
@Test
public void distanceProfilesStayContinuousWhenSearchCenterChanges() {
FastNoiseDouble noise = new FastNoiseDouble(2320148477032134941L);
noise.setFrequency(1D);
noise.setCellularDistanceFunction(FastNoiseDouble.CellularDistanceFunction.Natural);
for (FastNoiseDouble.CellularReturnType returnType : TWO_EDGE_RETURN_TYPES) {
noise.setCellularReturnType(returnType);
double below = noise.GetCellular(783.44443982D, 1896.5D - 1E-8D);
double above = noise.GetCellular(783.44443982D, 1896.5D + 1E-8D);
assertTrue(Math.abs(above - below) < 1E-6D);
}
}
private double referenceGetCellular2D(long seed, double frequency, FastNoiseDouble.CellularDistanceFunction distanceFunction, FastNoiseDouble.CellularReturnType returnType, double x, double y) throws Exception {
double scaledX = x * frequency;
double scaledY = y * frequency;
long xr = fastRound(scaledX);
@@ -87,8 +125,8 @@ public class FastNoiseDoubleCellularParityTest {
double distance = 999999D;
double distance2 = 999999D;
for (long xi = xr - 1; xi <= xr + 1; xi++) {
for (long yi = yr - 1; yi <= yr + 1; yi++) {
for (long xi = xr - 2; xi <= xr + 2; xi++) {
for (long yi = yr - 2; yi <= yr + 2; yi++) {
int cellIndex = (int) hash2D(seed, xi, yi) & 255;
double vecX = xi - scaledX + CELL_2D_X[cellIndex];
double vecY = yi - scaledY + CELL_2D_Y[cellIndex];
@@ -105,7 +143,7 @@ public class FastNoiseDoubleCellularParityTest {
return applyTwoEdgeReturn(distance, distance2, returnType);
}
private double legacyGetCellular3D(long seed, double frequency, FastNoiseDouble.CellularDistanceFunction distanceFunction, FastNoiseDouble.CellularReturnType returnType, double x, double y, double z) throws Exception {
private double referenceGetCellular3D(long seed, double frequency, FastNoiseDouble.CellularDistanceFunction distanceFunction, FastNoiseDouble.CellularReturnType returnType, double x, double y, double z) throws Exception {
double scaledX = x * frequency;
double scaledY = y * frequency;
double scaledZ = z * frequency;
@@ -114,10 +152,11 @@ public class FastNoiseDoubleCellularParityTest {
long zr = fastRound(scaledZ);
double distance = 999999D;
double distance2 = 999999D;
int radius = distanceFunction == FastNoiseDouble.CellularDistanceFunction.Manhattan ? 3 : 2;
for (long xi = xr - 1; xi <= xr + 1; xi++) {
for (long yi = yr - 1; yi <= yr + 1; yi++) {
for (long zi = zr - 1; zi <= zr + 1; zi++) {
for (long xi = xr - radius; xi <= xr + radius; xi++) {
for (long yi = yr - radius; yi <= yr + radius; yi++) {
for (long zi = zr - radius; zi <= zr + radius; zi++) {
int cellIndex = (int) hash3D(seed, xi, yi, zi) & 255;
double vecX = xi - scaledX + CELL_3D_X[cellIndex];
double vecY = yi - scaledY + CELL_3D_Y[cellIndex];
@@ -0,0 +1,33 @@
package art.arcane.iris.util.project.noise;
import org.junit.Test;
import java.lang.reflect.Method;
import static org.junit.Assert.assertEquals;
public class FastNoiseFloorTest {
@Test
public void doubleLatticeFloorHandlesNegativeIntegersAndNeighbors() throws Exception {
Method floor = FastNoiseDouble.class.getDeclaredMethod("fastFloor", double.class);
floor.setAccessible(true);
assertEquals(-1L, ((Long) floor.invoke(null, -1D)).longValue());
assertEquals(-2L, ((Long) floor.invoke(null, -2D)).longValue());
assertEquals(-2L, ((Long) floor.invoke(null, Math.nextDown(-1D))).longValue());
assertEquals(-1L, ((Long) floor.invoke(null, Math.nextUp(-1D))).longValue());
assertEquals(0L, ((Long) floor.invoke(null, -0D)).longValue());
assertEquals(Long.MIN_VALUE, ((Long) floor.invoke(null, (double) Long.MIN_VALUE)).longValue());
}
@Test
public void floatLatticeFloorHandlesNegativeIntegersAndNeighbors() throws Exception {
Method floor = FastNoise.class.getDeclaredMethod("FastFloor", float.class);
floor.setAccessible(true);
assertEquals(-1, ((Integer) floor.invoke(null, -1F)).intValue());
assertEquals(-2, ((Integer) floor.invoke(null, -2F)).intValue());
assertEquals(-2, ((Integer) floor.invoke(null, Math.nextDown(-1F))).intValue());
assertEquals(-1, ((Integer) floor.invoke(null, Math.nextUp(-1F))).intValue());
assertEquals(0, ((Integer) floor.invoke(null, -0F)).intValue());
assertEquals(Integer.MIN_VALUE, ((Integer) floor.invoke(null, (float) Integer.MIN_VALUE)).intValue());
}
}
@@ -0,0 +1,156 @@
package art.arcane.iris.util.project.noise;
import org.junit.Test;
import java.util.Random;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class GeometricNoiseTest {
private static final double SQRT_3 = Math.sqrt(3D);
private static final long[] SEEDS = {0L, -1L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE};
@Test
public void regularHexagonsMatchNearestLatticeCenters() {
NoiseGenerator[] noises = {new HexagonNoise(177L), new HexSimplexNoise(311L)};
for (NoiseGenerator noise : noises) {
for (double x = -5.93D; x < 6D; x += 0.137D) {
for (double z = -5.97D; z < 6D; z += 0.173D) {
double bestDistance = Double.POSITIVE_INFINITY;
double centerX = 0D;
double centerZ = 0D;
for (int q = -8; q <= 8; q++) {
for (int r = -8; r <= 8; r++) {
double candidateX = SQRT_3 * (q + (r * 0.5D));
double candidateZ = 1.5D * r;
double dx = x - candidateX;
double dz = z - candidateZ;
double distance = (dx * dx) + (dz * dz);
if (distance < bestDistance) {
bestDistance = distance;
centerX = candidateX;
centerZ = candidateZ;
}
}
}
assertEquals(noise.noise(centerX, centerZ), noise.noise(x, z), 0D);
assertEquals(noise.noise(centerX, 2.75D, centerZ), noise.noise(x, 2.75D, z), 0D);
}
}
}
}
@Test
public void hexagonNeighborsFollowCoherentHeat() {
HexagonNoise noise = new HexagonNoise(7331L);
double adjacentDifference = 0D;
double distantDifference = 0D;
for (int q = -32; q <= 32; q++) {
for (int r = -32; r <= 32; r++) {
double x = SQRT_3 * (q + (r * 0.5D));
double z = 1.5D * r;
double sample = noise.noise(x, z);
adjacentDifference += Math.abs(sample - noise.noise(x + SQRT_3, z));
distantDifference += Math.abs(sample - noise.noise(x + (SQRT_3 * 19D), z));
}
}
assertTrue(adjacentDifference < distantDifference * 0.85D);
}
@Test
public void nestedHexagonsHaveRegularInscribedFootprints() {
for (int child = 0; child < 7; child++) {
double angle = Math.toRadians((child * 60D) - 90D);
double centerX = child == 0 ? 0D : Math.cos(angle) * (2D / 3D);
double centerZ = child == 0 ? 0D : Math.sin(angle) * (2D / 3D);
for (int direction = 0; direction < 60; direction++) {
double sampleAngle = Math.toRadians(direction * 6D);
double x = centerX + (Math.cos(sampleAngle) * 0.27D);
double z = centerZ + (Math.sin(sampleAngle) * 0.27D);
double q = (x / SQRT_3) - (z / 3D);
double r = 2D * z / 3D;
assertEquals(child, HexagonalNoise.pickChildIndex(q, r));
assertEquals(0L, HexagonalNoise.roundQ(q, r));
assertEquals(0L, HexagonalNoise.roundR(q, r));
}
}
}
@Test
public void sierpinskiCutsTriangleCentroidsAtEveryDepth() {
SierpinskiTriangleNoise noise = new SierpinskiTriangleNoise(91L);
for (int depth = 0; depth < 4; depth++) {
double size = Math.scalb(1D, -depth);
double holeU = size / 3D;
double holeV = size / 3D;
assertTrue(sampleTriangle(noise, holeU, holeV) <= 0.12D);
assertTrue(sampleTriangle(noise, 1D - size + holeU, holeV) <= 0.12D);
assertTrue(sampleTriangle(noise, holeU, 1D - size + holeV) <= 0.12D);
}
assertTrue(sampleTriangle(noise, 1D / 48D, 1D / 48D) >= 0.35D);
assertTrue(sampleTriangle(noise, 46D / 48D, 1D / 48D) >= 0.35D);
assertTrue(sampleTriangle(noise, 1D / 48D, 46D / 48D) >= 0.35D);
}
@Test
public void sierpinskiTilesAcrossNegativeCoordinatesWithExpectedArea() {
SierpinskiTriangleNoise noise = new SierpinskiTriangleNoise(9123L);
Random random = new Random(8211L);
int filled = 0;
int samples = 65536;
for (int index = 0; index < samples; index++) {
double u = random.nextDouble();
double v = random.nextDouble();
double x = 8D * (u + (v * 0.5D));
double z = 4D * SQRT_3 * v;
boolean inside = noise.noise(x, z) >= 0.35D;
if (inside) {
filled++;
}
assertEquals(inside, noise.noise(x - 8D, z) >= 0.35D);
assertEquals(inside, noise.noise(x - 4D, z - (4D * SQRT_3)) >= 0.35D);
}
assertEquals(Math.pow(0.75D, 4), filled / (double) samples, 0.006D);
}
@Test
public void dimensionalSlicesAreConsistentAndContinuous() {
for (NoiseGenerator noise : generators(1337L)) {
assertEquals(noise.noise(3.125D), noise.noise(3.125D, 0D), 0D);
assertEquals(noise.noise(3.125D, -1.75D), noise.noise(3.125D, 0D, -1.75D), 0D);
assertEquals(noise.noise(3.125D, 1.75D, 0D), noise.noise(3.125D, 1.75D, 1E-9D), 1E-7D);
assertEquals(noise.noise(3.125D, 0D, -1.75D), noise.noise(3.125D, 1E-9D, -1.75D), 1E-7D);
}
}
@Test
public void seedsAndWorldBorderCoordinatesRemainDeterministicAndBounded() {
double[][] points = {{0D, 0D, 0D}, {-29_999_984D, -64D, 29_999_984D},
{3.125D, 17.375D, -9.25D}, {29_999_984D, 319D, -29_999_984D}};
for (long seed : SEEDS) {
NoiseGenerator[] first = generators(seed);
NoiseGenerator[] second = generators(seed);
for (int index = 0; index < first.length; index++) {
if (first[index] instanceof OctaveNoise octaves) {
octaves.setOctaves(16);
((OctaveNoise) second[index]).setOctaves(16);
}
for (double[] point : points) {
double value = first[index].noise(point[0], point[1], point[2]);
assertTrue(Double.isFinite(value) && value >= 0D && value <= 1D);
assertEquals(value, second[index].noise(point[0], point[1], point[2]), 0D);
}
}
}
}
private double sampleTriangle(SierpinskiTriangleNoise noise, double u, double v) {
return noise.noise(8D * (u + (v * 0.5D)), 4D * SQRT_3 * v);
}
private NoiseGenerator[] generators(long seed) {
return new NoiseGenerator[]{new HexagonNoise(seed), new HexSimplexNoise(seed),
new HexRandomSizeNoise(seed), new HexJamesNoise(seed), new SierpinskiTriangleNoise(seed)};
}
}
@@ -0,0 +1,87 @@
package art.arcane.iris.util.project.noise;
import art.arcane.iris.util.project.interpolation.InterpolationMethod;
import art.arcane.iris.util.project.interpolation.IrisInterpolation;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class InterpolatedNoiseDimensionTest {
@Test
public void threeDimensionalSamplesAlwaysUseTheHorizontalPlane() {
InterpolatedNoise noise = new InterpolatedNoise(37L, NoiseType.WHITE, InterpolationMethod.BILINEAR);
for (double x : new double[]{-13D, 0D, 41D}) {
for (double y : new double[]{-89D, 0D, 173D}) {
for (double z : new double[]{-37D, 0D, 29D}) {
assertEquals(noise.noise(x, z), noise.noise(x, y, z), 0D);
}
}
assertEquals(noise.noise(x, 0D), noise.noise(x), 0D);
}
}
@Test
public void interpolationSamplesTheNormalizedSourceAndForwardsOctaves() {
long seed = 93L;
NoiseType type = NoiseType.SIMPLEX;
NoiseGenerator source = type.create(seed);
((OctaveNoise) source).setOctaves(4);
InterpolatedNoise noise = new InterpolatedNoise(seed, type, InterpolationMethod.BILINEAR);
noise.setOctaves(4);
double scale = type.getCoordinateScale();
for (int x = -49; x <= 49; x += 7) {
for (int z = -51; z <= 51; z += 11) {
double expected = IrisInterpolation.getNoise(InterpolationMethod.BILINEAR, x, z, 32D,
(sampleX, sampleZ) -> source.noise(sampleX * scale, sampleZ * scale));
assertEquals(expected, noise.noise(x, z), 1.0E-12D);
}
}
}
@Test
public void fractionalCoordinatesReachEveryInterpolationKernel() {
long seed = 109L;
NoiseType type = NoiseType.SIMPLEX;
NoiseGenerator source = type.create(seed);
double scale = type.getCoordinateScale();
for (InterpolationMethod method : InterpolationMethod.values()) {
InterpolatedNoise noise = new InterpolatedNoise(seed, type, method);
double expected = IrisInterpolation.getNoise(method, -7.75D, 3.25D, 32D,
(x, z) -> source.noise(x * scale, z * scale));
double actual = noise.noise(-7.75D, 3.25D);
assertEquals(method.name(), expected, actual, 0D);
assertNotEquals(method.name(), noise.noise(-7D, 3D), actual, 1.0E-9D);
}
}
@Test
public void noiseWrappersConstrainCubicOvershootWithoutConstrainingTheKernel() {
double overshoot = IrisInterpolation.getNoise(InterpolationMethod.BICUBIC, 16D, 16D, 32D,
(x, z) -> x >= 0D && x <= 32D ? 1D : 0D);
assertEquals(1.25D, overshoot, 0D);
for (NoiseType type : new NoiseType[]{NoiseType.WHITE, NoiseType.CELLULAR, NoiseType.CLOVER}) {
for (InterpolationMethod method : new InterpolationMethod[]{InterpolationMethod.BICUBIC,
InterpolationMethod.HERMITE}) {
InterpolatedNoise noise = new InterpolatedNoise(173L, type, method);
boolean reachesEndpoint = false;
for (double x = -256D; x <= 256D; x += 7.375D) {
for (double z = -256D; z <= 256D; z += 5.5D) {
double value = noise.noise(x, z);
assertTrue(type.name() + "/" + method.name(), value >= 0D && value <= 1D);
reachesEndpoint |= value == 0D || value == 1D;
}
}
if (type == NoiseType.WHITE) {
assertTrue(method.name(), reachesEndpoint);
}
}
}
}
}
@@ -0,0 +1,104 @@
package art.arcane.iris.util.project.noise;
import art.arcane.volmlib.util.math.RNG;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class NoiseOctaveTest {
private static final long SEED = 972453L;
private static final double X = 37.219D;
private static final double Y = -91.713D;
private static final double Z = 152.875D;
@Test
public void simplexAndPerlinKeepFundamentalAndReduceDetailAmplitude() {
for (boolean simplex : new boolean[]{false, true}) {
NoiseGenerator noise = simplex ? new SimplexNoise(SEED) : new PerlinNoise(SEED);
((OctaveNoise) noise).setOctaves(3);
FastNoiseDouble source = new FastNoiseDouble(new RNG(SEED).lmax());
source.setLongerp(FastNoiseDouble.Longerp.Qulongic);
for (int dimensions = 1; dimensions <= 3; dimensions++) {
double expected = 0D;
for (int octave = 0; octave < 3; octave++) {
double frequency = 1 << octave;
double sample;
if (dimensions == 3) {
sample = simplex ? source.GetSimplex(X * frequency, Y * frequency, Z * frequency)
: source.GetPerlin(X * frequency, Y * frequency, Z * frequency);
} else {
double second = dimensions == 1 ? 0D : Z * frequency;
sample = simplex ? source.GetSimplex(X * frequency, second)
: source.GetPerlin(X * frequency, second);
}
expected += sample / frequency;
}
expected /= 1.75D;
double signed = switch (dimensions) {
case 1 -> noise.noiseSigned(X);
case 2 -> noise.noiseSigned(X, Z);
default -> noise.noiseSigned(X, Y, Z);
};
double unsigned = switch (dimensions) {
case 1 -> noise.noise(X);
case 2 -> noise.noise(X, Z);
default -> noise.noise(X, Y, Z);
};
assertEquals(expected, signed, 1E-15D);
assertEquals(signed / 2D + 0.5D, unsigned, 0D);
}
assertEquals(noise.noise(X, 0D), noise.noise(X), 0D);
}
}
@Test(timeout = 5000)
public void octaveWrappersClampInvalidAndExcessiveCounts() {
NoiseGenerator[] generators = {
new SimplexNoise(SEED), new PerlinNoise(SEED), new FractalFBMSimplexNoise(SEED),
new FractalBillowSimplexNoise(SEED), new FractalBillowPerlinNoise(SEED),
new FractalRigidMultiSimplexNoise(SEED), new FractalCubicNoise(SEED)
};
for (NoiseGenerator noise : generators) {
OctaveNoise octaveNoise = (OctaveNoise) noise;
octaveNoise.setOctaves(1);
double minimum = noise.noise(X, Y, Z);
octaveNoise.setOctaves(Integer.MIN_VALUE);
assertEquals(minimum, noise.noise(X, Y, Z), 0D);
octaveNoise.setOctaves(16);
double maximum = noise.noise(X, Y, Z);
octaveNoise.setOctaves(Integer.MAX_VALUE);
assertEquals(maximum, noise.noise(X, Y, Z), 0D);
assertTrue(Double.isFinite(maximum));
}
}
@Test
public void cubicFractalUsesRequestedOctaves() {
FractalCubicNoise noise = new FractalCubicNoise(SEED);
noise.setOctaves(1);
double fundamental = noise.noise(X, Y, Z);
noise.setOctaves(4);
double detailed = noise.noise(X, Y, Z);
assertNotEquals(fundamental, detailed, 1E-8D);
FastNoiseDouble source = new FastNoiseDouble(new RNG(SEED).lmax());
source.setFractalType(FastNoiseDouble.FractalType.Billow);
source.setFractalOctaves(4);
assertEquals(source.GetCubicFractal(X, Y, Z) / 2D + 0.5D, detailed, 0D);
}
@Test(timeout = 5000)
public void rawFractalSamplersBoundOctaveWork() {
FastNoiseDouble doubles = new FastNoiseDouble(SEED);
FastNoise floats = new FastNoise((int) SEED);
doubles.setFractalOctaves(16);
floats.SetFractalOctaves(16);
double expectedDouble = doubles.GetSimplexFractal(X, Y, Z);
float expectedFloat = floats.GetSimplexFractal((float) X, (float) Y, (float) Z);
doubles.setFractalOctaves(Long.MAX_VALUE);
floats.SetFractalOctaves(Integer.MAX_VALUE);
assertEquals(expectedDouble, doubles.GetSimplexFractal(X, Y, Z), 0D);
assertEquals(expectedFloat, floats.GetSimplexFractal((float) X, (float) Y, (float) Z), 0D);
}
}
@@ -0,0 +1,40 @@
package art.arcane.iris.util.project.noise;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
public class WhiteNoiseTest {
@Test
public void scatterDoesNotRepeatEvery8192Blocks() {
WhiteNoise noise = new WhiteNoise(7153L);
for (double x : new double[]{-29999999.25D, -8192D, 0D, 37.5D, 8192D}) {
assertNotEquals(noise.noise(x, 73D), noise.noise(x + 8192D, 73D), 0D);
assertNotEquals(noise.noise(x, 53D, 73D), noise.noise(x, 53D, 73D + 8192D), 0D);
}
}
@Test
public void fractionalCoordinatesRemainDistinctNearWorldBorder() {
WhiteNoise noise = new WhiteNoise(15273L);
double x = 29999111.25D;
assertNotEquals(noise.noise(x, -x), noise.noise(x + 0.0001D, -x), 0D);
assertNotEquals(noise.noise(x, 71.5D, -x), noise.noise(x, 71.5001D, -x), 0D);
}
@Test
public void scatterIsRepeatableAndUsesFullSeed() {
WhiteNoise first = new WhiteNoise(725L);
WhiteNoise same = new WhiteNoise(725L);
WhiteNoise upperBits = new WhiteNoise(725L + (1L << 48));
for (int index = 0; index < 512; index++) {
double x = index * 1.25D;
double value = first.noise(x, 53D, -x);
assertEquals(value, same.noise(x, 53D, -x), 0D);
assertTrue(value >= 0D && value <= 1D);
assertNotEquals(value, upperBits.noise(x, 53D, -x), 0D);
}
}
}