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 committed 2026-09-05 19:33:14 -04:00
1 parent 50894b90ce
commit 645edcff5c
13 files changed
+700 -1

No files matched your search

@@ -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);
}
}