diff --git a/core/src/main/java/art/arcane/iris/engine/object/NoiseStyle.java b/core/src/main/java/art/arcane/iris/engine/object/NoiseStyle.java index fd339091c..3febe384a 100644 --- a/core/src/main/java/art/arcane/iris/engine/object/NoiseStyle.java +++ b/core/src/main/java/art/arcane/iris/engine/object/NoiseStyle.java @@ -538,6 +538,21 @@ public enum NoiseStyle { @Desc("Vascular noise gets higher as the position nears a cell border. Cells are distorted using Iris styled wispy noise.") VASCULAR_IRIS_HALF(rng -> CNG.signatureHalf(rng, NoiseType.VASCULAR), NoiseType.VASCULAR), + + @Desc("Warped gyroid sheets form rounded mazes in 2D and interconnected labyrinths in 3D.") + GYROID(rng -> new CNG(rng, NoiseType.GYROID, 1D, 1), NoiseType.GYROID), + + @Desc("Fivefold wave interference forms non-repeating stars, rosettes, and crystalline contours.") + QUASICRYSTAL(rng -> new CNG(rng, NoiseType.QUASICRYSTAL, 1D, 1), NoiseType.QUASICRYSTAL), + + @Desc("Connected quarter-circle ribbons form tiled loops that twist continuously through height.") + TRUCHET(rng -> new CNG(rng, NoiseType.TRUCHET, 1D, 1), NoiseType.TRUCHET), + + @Desc("Scattered bowls with raised rims form crater fields in 2D and hollow shells in 3D.") + CRATER(rng -> new CNG(rng, NoiseType.CRATER, 1D, 1), NoiseType.CRATER), + + @Desc("Seeded spiral arms form swirling eddies in 2D and winding funnels in 3D.") + VORTEX(rng -> new CNG(rng, NoiseType.VORTEX, 1D, 1), NoiseType.VORTEX), ; private final CNGFactory f; diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/CraterNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/CraterNoise.java new file mode 100644 index 000000000..e358f08e7 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/CraterNoise.java @@ -0,0 +1,62 @@ +package art.arcane.iris.util.project.noise; + +public final class CraterNoise extends PatternNoise { + private static final double CENTER_SPREAD = 0.6D / 65535D; + private static final double MAX_SUPPORT_SQUARED = 0.6D * 0.6D * 1.35D; + + private final long seed; + + public CraterNoise(long seed) { + this.seed = seed; + } + + @Override + protected double sample(double x, double y, double z) { + long cellX = (long) Math.floor(x); + long cellY = (long) Math.floor(y); + long cellZ = (long) Math.floor(z); + double bowl = 0D; + double rim = 0D; + + for (int offsetY = -1; offsetY <= 1; offsetY++) { + long pointY = cellY + offsetY; + long hashY = pointY * 0xC2B2AE3D27D4EB4FL; + for (int offsetZ = -1; offsetZ <= 1; offsetZ++) { + long pointZ = cellZ + offsetZ; + long hashYZ = hashY ^ pointZ * 0x165667B19E3779F9L; + for (int offsetX = -1; offsetX <= 1; offsetX++) { + long pointX = cellX + offsetX; + long hash = mix(seed ^ pointX * 0x9E3779B97F4A7C15L ^ hashYZ); + double deltaX = x - (pointX + 0.2D + (hash & 65535L) * CENTER_SPREAD); + double deltaY = y - (pointY + 0.2D + ((hash >>> 16) & 65535L) * CENTER_SPREAD); + double deltaZ = z - (pointZ + 0.2D + ((hash >>> 32) & 65535L) * CENTER_SPREAD); + double distanceSquared = deltaX * deltaX + deltaY * deltaY + deltaZ * deltaZ; + if (distanceSquared >= MAX_SUPPORT_SQUARED) { + continue; + } + + double radius = 0.36D + (hash >>> 56) * (0.24D / 255D); + double relativeDistance = distanceSquared / (radius * radius); + if (relativeDistance >= 1.35D) { + continue; + } + + double depth = ((hash >>> 48) & 255L) / 255D; + double depression = Math.max(0D, 1D - relativeDistance); + bowl = Math.min(bowl, -(0.28D + depth * 0.17D) * depression * depression); + double rimDistance = (relativeDistance - 1D) / 0.35D; + double ridge = Math.max(0D, 1D - rimDistance * rimDistance); + rim = Math.max(rim, (0.18D + (1D - depth) * 0.12D) * ridge * ridge); + } + } + } + + return 0.5D + bowl + rim; + } + + private static long mix(long value) { + value = (value ^ (value >>> 30)) * 0xBF58476D1CE4E5B9L; + value = (value ^ (value >>> 27)) * 0x94D049BB133111EBL; + return value ^ (value >>> 31); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/GyroidNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/GyroidNoise.java new file mode 100644 index 000000000..5f7682e8e --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/GyroidNoise.java @@ -0,0 +1,44 @@ +package art.arcane.iris.util.project.noise; + +import java.util.SplittableRandom; + +public final class GyroidNoise extends PatternNoise { + private static final double TAU = Math.PI * 2D; + + private final double horizontalCosine; + private final double horizontalSine; + private final double tiltCosine; + private final double tiltSine; + private final double phaseX; + private final double phaseY; + private final double phaseZ; + + public GyroidNoise(long seed) { + SplittableRandom random = new SplittableRandom(seed); + double horizontalAngle = random.nextDouble(TAU); + double tiltAngle = random.nextDouble(0.35D, 1.2D); + horizontalCosine = Math.cos(horizontalAngle); + horizontalSine = Math.sin(horizontalAngle); + tiltCosine = Math.cos(tiltAngle); + tiltSine = Math.sin(tiltAngle); + phaseX = random.nextDouble(TAU); + phaseY = random.nextDouble(TAU); + phaseZ = random.nextDouble(TAU); + } + + @Override + protected double sample(double x, double y, double z) { + double horizontal = x * horizontalCosine + z * horizontalSine; + double transverse = z * horizontalCosine - x * horizontalSine; + double u = TAU * (horizontal * tiltCosine + y * tiltSine) + phaseX; + double v = TAU * (y * tiltCosine - horizontal * tiltSine) + phaseY; + double w = TAU * transverse + phaseZ; + double a = u + 0.7D * Math.sin(0.37D * v + 0.29D * w + phaseZ); + double b = v + 0.7D * Math.sin(0.31D * w - 0.23D * u + phaseX); + double c = w + 0.7D * Math.sin(0.41D * u + 0.19D * v + phaseY); + double field = Math.sin(a) * Math.cos(b) + Math.sin(b) * Math.cos(c) + Math.sin(c) * Math.cos(a); + double width = 0.62D + 0.22D * Math.sin(0.29D * u + 0.23D * v - 0.19D * w); + double sheet = Math.max(0D, 1D - Math.abs(field) / width); + return sheet * sheet * (3D - 2D * sheet); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/NoiseType.java b/core/src/main/java/art/arcane/iris/util/project/noise/NoiseType.java index 0dbd95513..78c6e0079 100644 --- a/core/src/main/java/art/arcane/iris/util/project/noise/NoiseType.java +++ b/core/src/main/java/art/arcane/iris/util/project/noise/NoiseType.java @@ -87,7 +87,12 @@ public enum NoiseType { SIERPINSKI_TRIANGLE_BILINEAR((s) -> new InterpolatedNoise(s, SIERPINSKI_TRIANGLE, InterpolationMethod.BILINEAR), 1D), SIERPINSKI_TRIANGLE_BICUBIC((s) -> new InterpolatedNoise(s, SIERPINSKI_TRIANGLE, InterpolationMethod.BICUBIC), 1D), SIERPINSKI_TRIANGLE_HERMITE((s) -> new InterpolatedNoise(s, SIERPINSKI_TRIANGLE, InterpolationMethod.HERMITE), 1D), - VASCULAR(VascularNoise::new); + VASCULAR(VascularNoise::new), + GYROID(GyroidNoise::new, 1D / 64D), + QUASICRYSTAL(QuasicrystalNoise::new, 1D / 64D), + TRUCHET(TruchetNoise::new, 1D / 64D), + CRATER(CraterNoise::new, 1D / 64D), + VORTEX(VortexNoise::new, 1D / 64D); private final NoiseFactory f; private final double coordinateScale; diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/PatternNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/PatternNoise.java new file mode 100644 index 000000000..90921892a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/PatternNoise.java @@ -0,0 +1,40 @@ +package art.arcane.iris.util.project.noise; + +abstract class PatternNoise implements NoiseGenerator, OctaveNoise { + private int octaves = 1; + private double bounding = 1D; + + @Override + public final double noise(double x) { + return noise(x, 0D); + } + + @Override + public final double noise(double x, double z) { + return noise(x, 0D, z); + } + + @Override + public final double noise(double x, double y, double z) { + if (octaves == 1) { + return sample(x, y, z); + } + double sum = 0D; + double frequency = 1D; + double amplitude = 1D; + for (int octave = 0; octave < octaves; octave++) { + sum += sample(x * frequency, y * frequency, z * frequency) * amplitude; + frequency *= 2D; + amplitude *= 0.5D; + } + return sum * bounding; + } + + @Override + public final void setOctaves(int octaves) { + this.octaves = Math.clamp(octaves, 1, 16); + bounding = 1D / (2D - Math.scalb(1D, 1 - this.octaves)); + } + + protected abstract double sample(double x, double y, double z); +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/QuasicrystalNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/QuasicrystalNoise.java new file mode 100644 index 000000000..1c78abd96 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/QuasicrystalNoise.java @@ -0,0 +1,37 @@ +package art.arcane.iris.util.project.noise; + +import java.util.SplittableRandom; + +public final class QuasicrystalNoise extends PatternNoise { + private static final int WAVES = 5; + private static final double TAU = Math.PI * 2D; + + private final double[] waveX = new double[WAVES]; + private final double[] waveZ = new double[WAVES]; + private final double phase; + private final double verticalFrequency; + private final double contourPhase; + + public QuasicrystalNoise(long seed) { + SplittableRandom random = new SplittableRandom(seed); + double rotation = random.nextDouble(TAU); + for (int i = 0; i < WAVES; i++) { + double angle = rotation + i * TAU / WAVES; + waveX[i] = Math.cos(angle) * TAU; + waveZ[i] = Math.sin(angle) * TAU; + } + phase = random.nextDouble(TAU); + verticalFrequency = random.nextDouble(0.3D, 0.7D) * TAU; + contourPhase = random.nextDouble(TAU); + } + + @Override + protected double sample(double x, double y, double z) { + double layerPhase = phase + y * verticalFrequency; + double sum = 0D; + for (int i = 0; i < WAVES; i++) { + sum += Math.cos(x * waveX[i] + z * waveZ[i] + layerPhase); + } + return 0.5D + 0.5D * Math.sin(sum * 1.4D + contourPhase); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/TruchetNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/TruchetNoise.java new file mode 100644 index 000000000..2c4fb2913 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/TruchetNoise.java @@ -0,0 +1,49 @@ +package art.arcane.iris.util.project.noise; + +public final class TruchetNoise extends PatternNoise { + private static final double RIBBON_WIDTH = 0.075D; + private static final double HEIGHT_FREQUENCY = Math.PI * 0.25D; + + private final long seed; + private final double phaseX; + private final double phaseZ; + + public TruchetNoise(long seed) { + this.seed = seed; + phaseX = 0.17D + (mix(seed) >>> 11) * 0x1.0p-53D * 0.61D; + phaseZ = 0.13D + (mix(seed + 0x9E3779B97F4A7C15L) >>> 11) * 0x1.0p-53D * 0.73D; + } + + @Override + protected double sample(double x, double y, double z) { + x += phaseX; + z += phaseZ; + if (y != 0D) { + double angle = y * HEIGHT_FREQUENCY; + x += Math.sin(angle) * 0.3D + y * 0.125D; + z += (Math.cos(angle) - 1D) * 0.3D; + } + + long cellX = (long) Math.floor(x); + long cellZ = (long) Math.floor(z); + double localX = x - cellX; + double localZ = z - cellZ; + long orientation = mix(seed ^ cellX * 0x9E3779B97F4A7C15L ^ cellZ * 0xC2B2AE3D27D4EB4FL); + if ((orientation & 1L) != 0L) { + localX = 1D - localX; + } + + double oppositeX = 1D - localX; + double oppositeZ = 1D - localZ; + double first = Math.abs(Math.sqrt(localX * localX + localZ * localZ) - 0.5D); + double second = Math.abs(Math.sqrt(oppositeX * oppositeX + oppositeZ * oppositeZ) - 0.5D); + double ribbon = Math.max(0D, 1D - Math.min(first, second) / RIBBON_WIDTH); + return ribbon * ribbon * (3D - 2D * ribbon); + } + + private static long mix(long value) { + value = (value ^ (value >>> 30)) * 0xBF58476D1CE4E5B9L; + value = (value ^ (value >>> 27)) * 0x94D049BB133111EBL; + return value ^ (value >>> 31); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/VortexNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/VortexNoise.java new file mode 100644 index 000000000..c3e21ace3 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/VortexNoise.java @@ -0,0 +1,63 @@ +package art.arcane.iris.util.project.noise; + +public final class VortexNoise extends PatternNoise { + private static final double RADIUS = 0.9D; + private static final double RADIUS_SQUARED = RADIUS * RADIUS; + private static final double TAU = Math.PI * 2D; + + private final long seed; + + public VortexNoise(long seed) { + this.seed = seed; + } + + @Override + protected double sample(double x, double y, double z) { + long cellX = (long) Math.floor(x); + long cellZ = (long) Math.floor(z); + double sum = 0D; + double weightSum = 0.07D; + for (int offsetX = -1; offsetX <= 1; offsetX++) { + long gridX = cellX + offsetX; + for (int offsetZ = -1; offsetZ <= 1; offsetZ++) { + long gridZ = cellZ + offsetZ; + long hash = hash(gridX, gridZ); + double dx = x - (gridX + 0.2D + unit(hash) * 0.6D); + hash = mix(hash); + double dz = z - (gridZ + 0.2D + unit(hash) * 0.6D); + double radiusSquared = dx * dx + dz * dz; + if (radiusSquared >= RADIUS_SQUARED) { + continue; + } + double radius = Math.sqrt(radiusSquared); + double envelope = 1D - radiusSquared / RADIUS_SQUARED; + double weight = envelope * envelope; + hash = mix(hash); + int arms = 2 + (int) (hash & 1L); + int direction = (hash & 2L) == 0L ? -1 : 1; + double phase = TAU * unit(hash); + hash = mix(hash); + double winding = 12D + unit(hash) * 4D; + double wave = Math.sin(direction * arms * Math.atan2(dz, dx) + - winding * radius + phase + y * TAU * 0.5D); + sum += weight * radius / (radius + 0.06D) * wave; + weightSum += weight; + } + } + return 0.5D + 0.5D * sum / weightSum; + } + + private long hash(long x, long z) { + return mix(seed ^ x * 0x9E3779B97F4A7C15L ^ z * 0xD1B54A32D192ED03L); + } + + private static long mix(long value) { + value = (value ^ (value >>> 30)) * 0xBF58476D1CE4E5B9L; + value = (value ^ (value >>> 27)) * 0x94D049BB133111EBL; + return value ^ (value >>> 31); + } + + private static double unit(long value) { + return (value >>> 11) * 0x1.0p-53; + } +} diff --git a/core/src/test/java/art/arcane/iris/util/project/noise/CraterNoiseTest.java b/core/src/test/java/art/arcane/iris/util/project/noise/CraterNoiseTest.java new file mode 100644 index 000000000..80c452e88 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/noise/CraterNoiseTest.java @@ -0,0 +1,82 @@ +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 CraterNoiseTest { + @Test + public void craterCentersSitInsideRaisedSphericalRims() { + CraterNoise noise = new CraterNoise(0L); + double center = noise.noise(0.2D, 0.2D, 0.2D); + + assertTrue(center < 0.4D); + assertTrue(noise.noise(0.56D, 0.2D, 0.2D) > center + 0.2D); + assertTrue(noise.noise(0.2D, 0.56D, 0.2D) > center + 0.2D); + assertTrue(noise.noise(0.2D, 0.2D, 0.56D) > center + 0.2D); + } + + @Test + public void neighboringCellSearchDoesNotLeaveTileSeams() { + double epsilon = 1.0E-8D; + for (long seed : new long[]{0L, 713L, Long.MIN_VALUE}) { + CraterNoise noise = new CraterNoise(seed); + for (int boundary = -3; boundary <= 3; boundary++) { + for (int index = 0; index < 64; index++) { + double first = index * 0.071D - 2D; + double second = index * -0.093D + 3D; + assertEquals(noise.noise(boundary - epsilon, first, second), + noise.noise(boundary + epsilon, first, second), 1.0E-5D); + assertEquals(noise.noise(first, boundary - epsilon, second), + noise.noise(first, boundary + epsilon, second), 1.0E-5D); + assertEquals(noise.noise(first, second, boundary - epsilon), + noise.noise(first, second, boundary + epsilon), 1.0E-5D); + } + } + } + } + + @Test + public void horizontalSliceHasDistinctBowlsAndAnnularRims() { + CraterNoise noise = new CraterNoise(713L); + CraterNoise otherSeed = new CraterNoise(714L); + double minimum = 1D; + double maximum = 0D; + double heightVariation = 0D; + double seedVariation = 0D; + + for (int index = 0; index < 1024; index++) { + double x = index * 0.037D - 4D; + double z = index * -0.051D + 7D; + double value = noise.noise(x, z); + assertEquals(value, noise.noise(x, 0D, z), 0D); + minimum = Math.min(minimum, value); + maximum = Math.max(maximum, value); + heightVariation += Math.abs(value - noise.noise(x, 1.25D, z)); + seedVariation += Math.abs(noise.noise(x) - otherSeed.noise(x)); + } + + assertTrue(minimum < 0.3D); + assertTrue(maximum > 0.7D); + assertTrue(heightVariation > 10D); + assertTrue(seedVariation > 10D); + } + + @Test + public void octaveSamplesStayBoundedAndRepeatableAtWorldBorders() { + CraterNoise first = new CraterNoise(Long.MAX_VALUE); + CraterNoise second = new CraterNoise(Long.MAX_VALUE); + + for (int octaves : new int[]{1, 4, 16}) { + first.setOctaves(octaves); + second.setOctaves(octaves); + for (double coordinate : new double[]{-468750D, -1D, -0D, 0.125D, 1D, 468750D}) { + double value = first.noise(coordinate, coordinate * 0.37D, coordinate * -0.71D); + assertTrue(Double.isFinite(value)); + assertTrue(value >= 0D && value <= 1D); + assertEquals(value, second.noise(coordinate, coordinate * 0.37D, coordinate * -0.71D), 0D); + } + } + } +} diff --git a/core/src/test/java/art/arcane/iris/util/project/noise/PatternNoiseTest.java b/core/src/test/java/art/arcane/iris/util/project/noise/PatternNoiseTest.java new file mode 100644 index 000000000..e2e88dfbb --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/noise/PatternNoiseTest.java @@ -0,0 +1,110 @@ +package art.arcane.iris.util.project.noise; + +import art.arcane.iris.engine.object.NoiseStyle; +import art.arcane.volmlib.util.math.RNG; +import org.junit.Test; + +import java.util.Random; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class PatternNoiseTest { + private static final NoiseStyle[] STYLES = { + NoiseStyle.GYROID, NoiseStyle.QUASICRYSTAL, NoiseStyle.TRUCHET, + NoiseStyle.CRATER, NoiseStyle.VORTEX + }; + + @Test + public void patternsRemainBoundedAtEveryOctaveCountAndWorldEdge() { + Random random = new Random(1337L); + for (long seed : new long[]{0L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE}) { + for (PatternNoise noise : patterns(seed)) { + for (int octaves : new int[]{1, 2, 5, 16}) { + noise.setOctaves(octaves); + for (int sample = 0; sample < 128; sample++) { + double x = (random.nextDouble() * 2D - 1D) * 30_000_000D / 64D; + double y = (random.nextDouble() * 2D - 1D) * 2048D / 64D; + double z = (random.nextDouble() * 2D - 1D) * 30_000_000D / 64D; + assertUnit(noise, noise.noise(x)); + assertUnit(noise, noise.noise(x, z)); + assertUnit(noise, noise.noise(x, y, z)); + assertEquals(noise.noise(x, z), noise.noise(x, 0D, z), 0D); + assertEquals(noise.noise(x), noise.noise(x, 0D), 0D); + } + } + } + } + } + + @Test + public void allPatternsUseTheFullSeedAndVaryThroughHeight() { + PatternNoise[] lowSeed = patterns(1337L); + PatternNoise[] sameSeed = patterns(1337L); + PatternNoise[] highSeed = patterns(1337L + (1L << 32)); + for (int index = 0; index < lowSeed.length; index++) { + double seedDifference = 0D; + double heightDifference = 0D; + for (int sample = 0; sample < 128; sample++) { + double x = sample * 0.173D - 8D; + double z = sample * -0.193D + 7D; + double value = lowSeed[index].noise(x, 0.371D, z); + assertEquals(value, sameSeed[index].noise(x, 0.371D, z), 0D); + seedDifference += Math.abs(value - highSeed[index].noise(x, 0.371D, z)); + heightDifference += Math.abs(value - lowSeed[index].noise(x, 0.891D, z)); + } + assertTrue(lowSeed[index].getClass().getSimpleName(), seedDifference > 1D); + assertTrue(lowSeed[index].getClass().getSimpleName(), heightDifference > 1D); + } + } + + @Test + public void octavesAddDetailWithoutChangingTheBaseSeed() { + for (PatternNoise noise : patterns(42L)) { + double[] original = new double[128]; + for (int index = 0; index < original.length; index++) { + original[index] = noise.noise(index * 0.073D, 0.419D, index * -0.113D); + } + noise.setOctaves(4); + double difference = 0D; + for (int index = 0; index < original.length; index++) { + difference += Math.abs(original[index] - noise.noise(index * 0.073D, 0.419D, index * -0.113D)); + } + assertTrue(noise.getClass().getSimpleName(), difference > 1D); + noise.setOctaves(0); + for (int index = 0; index < original.length; index++) { + assertEquals(original[index], noise.noise(index * 0.073D, 0.419D, index * -0.113D), 0D); + } + noise.setOctaves(16); + double maximum = noise.noise(0.713D, 0.931D, -0.719D); + noise.setOctaves(Integer.MAX_VALUE); + assertEquals(maximum, noise.noise(0.713D, 0.931D, -0.719D), 0D); + } + } + + @Test + public void styleIntegrationUsesTheSameScaleInEveryDimension() { + for (NoiseStyle style : STYLES) { + CNG direct = style.create(new RNG(7331L)); + CNG zoomed = style.style().zoomed(2D).createNoCache(new RNG(7331L), null); + for (int index = 0; index < 128; index++) { + double x = index * 13.713D - 400D; + double y = index * 1.119D - 70D; + double z = index * -17.179D + 300D; + assertEquals(style.name(), direct.noise(x, z), zoomed.noise(x * 2D, z * 2D), 1E-10D); + assertEquals(style.name(), direct.noise(x, y, z), zoomed.noise(x * 2D, y * 2D, z * 2D), 1E-10D); + assertEquals(style.name(), direct.noise(x, z), direct.noise(x, 0D, z), 0D); + } + } + } + + private static PatternNoise[] patterns(long seed) { + return new PatternNoise[]{new GyroidNoise(seed), new QuasicrystalNoise(seed), + new TruchetNoise(seed), new CraterNoise(seed), new VortexNoise(seed)}; + } + + private static void assertUnit(PatternNoise noise, double value) { + assertTrue(noise.getClass().getSimpleName() + ": " + value, + Double.isFinite(value) && value >= 0D && value <= 1D); + } +} diff --git a/core/src/test/java/art/arcane/iris/util/project/noise/TruchetNoiseTest.java b/core/src/test/java/art/arcane/iris/util/project/noise/TruchetNoiseTest.java new file mode 100644 index 000000000..9925a17ff --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/noise/TruchetNoiseTest.java @@ -0,0 +1,70 @@ +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 TruchetNoiseTest { + @Test + public void quarterCircleRibbonsMeetAcrossTileBoundaries() { + TruchetNoise xBoundaryNoise = new TruchetNoise(0L); + TruchetNoise zBoundaryNoise = new TruchetNoise(-0x9E3779B97F4A7C15L); + double epsilon = 1.0E-8D; + double maximum = 0D; + + for (int cell = -3; cell <= 3; cell++) { + for (int index = 0; index <= 2048; index++) { + double position = index / 512D - 2D; + double x = cell - 0.17D; + double z = cell - 0.13D; + assertEquals(xBoundaryNoise.noise(x - epsilon, position), + xBoundaryNoise.noise(x + epsilon, position), 1.0E-5D); + assertEquals(zBoundaryNoise.noise(position, z - epsilon), + zBoundaryNoise.noise(position, z + epsilon), 1.0E-5D); + maximum = Math.max(maximum, xBoundaryNoise.noise(x, position)); + } + } + + assertTrue(maximum > 0.999D); + } + + @Test + public void helicalContinuationIsContinuousThroughTheHorizontalSlice() { + TruchetNoise noise = new TruchetNoise(713L); + double heightVariation = 0D; + double seedVariation = 0D; + TruchetNoise otherSeed = new TruchetNoise(714L); + + for (int index = 0; index < 256; index++) { + double x = index * 0.037D - 4D; + double z = index * -0.051D + 7D; + double horizontal = noise.noise(x, z); + assertEquals(horizontal, noise.noise(x, 0D, z), 0D); + assertEquals(horizontal, noise.noise(x, -1.0E-8D, z), 1.0E-5D); + assertEquals(horizontal, noise.noise(x, 1.0E-8D, z), 1.0E-5D); + heightVariation += Math.abs(horizontal - noise.noise(x, 1.25D, z)); + seedVariation += Math.abs(noise.noise(x) - otherSeed.noise(x)); + } + + assertTrue(heightVariation > 10D); + assertTrue(seedVariation > 10D); + } + + @Test + public void octavesStayBoundedAndRepeatableAtWorldBorders() { + TruchetNoise first = new TruchetNoise(Long.MIN_VALUE); + TruchetNoise second = new TruchetNoise(Long.MIN_VALUE); + + for (int octaves : new int[]{1, 4, 16}) { + first.setOctaves(octaves); + second.setOctaves(octaves); + for (double coordinate : new double[]{-468750D, -1D, -0D, 0.125D, 1D, 468750D}) { + double value = first.noise(coordinate, coordinate * 0.37D, coordinate * -0.71D); + assertTrue(Double.isFinite(value)); + assertTrue(value >= 0D && value <= 1D); + assertEquals(value, second.noise(coordinate, coordinate * 0.37D, coordinate * -0.71D), 0D); + } + } + } +} diff --git a/core/src/test/java/art/arcane/iris/util/project/noise/VortexNoiseTest.java b/core/src/test/java/art/arcane/iris/util/project/noise/VortexNoiseTest.java new file mode 100644 index 000000000..a5bf94ed8 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/noise/VortexNoiseTest.java @@ -0,0 +1,37 @@ +package art.arcane.iris.util.project.noise; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class VortexNoiseTest { + @Test + public void spiralsStayContinuousAcrossCellBoundaries() { + for (long seed : new long[]{0L, 42L, Long.MAX_VALUE}) { + VortexNoise noise = new VortexNoise(seed); + for (int cell = -8; cell <= 8; cell++) { + for (int index = 0; index < 64; index++) { + double coordinate = index * 0.0713D - 2D; + assertEquals(noise.noise(cell - 1E-7D, 0.317D, coordinate), + noise.noise(cell + 1E-7D, 0.317D, coordinate), 1E-5D); + assertEquals(noise.noise(coordinate, 0.317D, cell - 1E-7D), + noise.noise(coordinate, 0.317D, cell + 1E-7D), 1E-5D); + } + } + } + } + + @Test + public void spiralCentersAndAngleWrapHaveNoJump() { + VortexNoise noise = new VortexNoise(0L); + double center = noise.noise(0.2D, 0.419D, 0.2D); + for (int index = 0; index < 128; index++) { + double angle = index * Math.PI / 64D; + assertEquals(center, noise.noise(0.2D + Math.cos(angle) * 1E-8D, 0.419D, + 0.2D + Math.sin(angle) * 1E-8D), 1E-6D); + double x = 0.2D - index * 0.007D; + assertEquals(noise.noise(x, 0.419D, 0.2D - 1E-8D), + noise.noise(x, 0.419D, 0.2D + 1E-8D), 1E-6D); + } + } +} diff --git a/core/src/test/java/art/arcane/iris/util/project/noise/WavePatternNoiseTest.java b/core/src/test/java/art/arcane/iris/util/project/noise/WavePatternNoiseTest.java new file mode 100644 index 000000000..35011d12d --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/noise/WavePatternNoiseTest.java @@ -0,0 +1,85 @@ +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 WavePatternNoiseTest { + private static final long SEED = 813753L; + + @Test + public void wavePatternsAreSeededBoundedAndContinuousInEveryAxis() { + for (boolean gyroid : new boolean[]{true, false}) { + NoiseGenerator first = gyroid ? new GyroidNoise(SEED) : new QuasicrystalNoise(SEED); + NoiseGenerator repeated = gyroid ? new GyroidNoise(SEED) : new QuasicrystalNoise(SEED); + NoiseGenerator otherSeed = gyroid ? new GyroidNoise(SEED + 1) : new QuasicrystalNoise(SEED + 1); + double seedDifference = 0D; + double horizontalDifference = 0D; + double verticalDifference = 0D; + double depthDifference = 0D; + for (double origin : new double[]{-30_000_000D / 64D, 0D, 30_000_000D / 64D}) { + for (int i = 0; i < 128; i++) { + double x = origin + i * 0.137D; + double y = i * 0.193D - 7D; + double z = -origin + i * 0.317D; + double value = first.noise(x, y, z); + assertTrue(Double.isFinite(value) && value >= 0D && value <= 1D); + assertEquals(value, repeated.noise(x, y, z), 0D); + assertEquals(value, first.noise(x + 1E-6D, y, z), 1E-3D); + assertEquals(value, first.noise(x, y + 1E-6D, z), 1E-3D); + assertEquals(value, first.noise(x, y, z + 1E-6D), 1E-3D); + seedDifference += Math.abs(value - otherSeed.noise(x, y, z)); + horizontalDifference += Math.abs(value - first.noise(x + 0.137D, y, z)); + verticalDifference += Math.abs(value - first.noise(x, y + 0.137D, z)); + depthDifference += Math.abs(value - first.noise(x, y, z + 0.137D)); + } + } + assertTrue(seedDifference > 10D); + assertTrue(horizontalDifference > 10D); + assertTrue(verticalDifference > 10D); + assertTrue(depthDifference > 10D); + } + } + + @Test + public void quasicrystalPreservesFivefoldRotationalSymmetryAcrossLayers() { + QuasicrystalNoise noise = new QuasicrystalNoise(SEED); + double angle = Math.PI * 2D / 5D; + double cosine = Math.cos(angle); + double sine = Math.sin(angle); + for (int i = 0; i < 128; i++) { + double x = i * 0.137D - 8D; + double y = i * 0.031D; + double z = i * 0.219D - 13D; + double rotatedX = x * cosine - z * sine; + double rotatedZ = x * sine + z * cosine; + assertEquals(noise.noise(x, y, z), noise.noise(rotatedX, y, rotatedZ), 1E-12D); + } + assertNotEquals(noise.noise(0.19D, -0.37D), noise.noise(1.19D, -0.37D), 1E-4D); + } + + @Test + public void gyroidContainsBothOpenSpacesAndBrightSheets() { + GyroidNoise noise = new GyroidNoise(SEED); + int open = 0; + int sheet = 0; + int gradient = 0; + for (int x = -64; x < 64; x++) { + for (int z = -64; z < 64; z++) { + double value = noise.noise(x * 0.127D, z * 0.127D); + if (value < 0.05D) { + open++; + } else if (value > 0.8D) { + sheet++; + } else { + gradient++; + } + } + } + assertTrue(open > 1638 && open < 13_107); + assertTrue(sheet > 819); + assertTrue(gradient > 1638); + } +}