Added patterned noise

- Gyroid and Quasicrystal provide labyrinths and wave interference
- Truchet tiles join quarter-circle ribbons
- Crater and Vortex provide bowls and spiral fields
- Pattern samplers share bounded octave accumulation
- Tests cover seeds, geometry, volume sampling, and world edges
This commit is contained in:
Brian Neumann-Fopiano
2026-09-05 19:33:14 -04:00
parent 50894b90ce
commit 645edcff5c
13 changed files with 700 additions and 1 deletions
@@ -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;
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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);
}
}
}
}
@@ -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);
}
}
@@ -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);
}
}
}
}
@@ -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);
}
}
}
@@ -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);
}
}