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 3febe384a..ec249d7d4 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 @@ -553,6 +553,36 @@ public enum NoiseStyle { @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), + + @Desc("Seeded crescent dunes with asymmetric slopes and smooth height-dependent drift.") + DUNE(rng -> new CNG(rng, NoiseType.DUNE, 1D, 1), NoiseType.DUNE), + + @Desc("Folded sedimentary bands with varying thickness through the volume.") + STRATA(rng -> new CNG(rng, NoiseType.STRATA, 1D, 1), NoiseType.STRATA), + + @Desc("Distorted growth rings and knot-like forms with variation through height.") + WOOD(rng -> new CNG(rng, NoiseType.WOOD, 1D, 1), NoiseType.WOOD), + + @Desc("Seeded oriented wave packets form a sparse directional ripple texture.") + GABOR(rng -> new CNG(rng, NoiseType.GABOR, 1D, 1), NoiseType.GABOR), + + @Desc("Turbulent stone veins separate broad smooth regions.") + MARBLE(rng -> new CNG(rng, NoiseType.MARBLE, 1D, 1), NoiseType.MARBLE), + + @Desc("Overlapping scalloped scales form a seeded tiled surface.") + SCALES(rng -> new CNG(rng, NoiseType.SCALES, 1D, 1), NoiseType.SCALES), + + @Desc("Standing-wave nodes form plate-like figures that change through height.") + CHLADNI(rng -> new CNG(rng, NoiseType.CHLADNI, 1D, 1), NoiseType.CHLADNI), + + @Desc("Mirrored wedge motifs repeat at a local scale with seeded variation.") + KALEIDOSCOPE(rng -> new CNG(rng, NoiseType.KALEIDOSCOPE, 1D, 1), NoiseType.KALEIDOSCOPE), + + @Desc("Recursive cubic voids form a three-dimensional sponge with square-hole slices.") + MENGER_SPONGE(rng -> new CNG(rng, NoiseType.MENGER_SPONGE, 1D, 1), NoiseType.MENGER_SPONGE), + + @Desc("Orthogonal tracks and ring pads form connected circuits that shift through height.") + CIRCUIT(rng -> new CNG(rng, NoiseType.CIRCUIT, 1D, 1), NoiseType.CIRCUIT), ; private final CNGFactory f; diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/ChladniNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/ChladniNoise.java new file mode 100644 index 000000000..5a6cc6e01 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/ChladniNoise.java @@ -0,0 +1,34 @@ +package art.arcane.iris.util.project.noise; + +public final class ChladniNoise extends PatternNoise { + private final int firstMode; + private final int secondMode; + private final double phase; + private final double width; + + public ChladniNoise(long seed) { + long hash = mix(seed); + firstMode = 2 + (int) (hash & 1L); + secondMode = firstMode + 1 + (int) ((hash >>> 1) & 1L); + phase = (hash >>> 11) * 0x1.0p-52D; + width = 0.22D + ((hash >>> 3) & 255L) * (0.12D / 255D); + } + + @Override + protected double sample(double x, double y, double z) { + double u = (x + phase) * Math.PI; + double v = (z + phase) * Math.PI; + double first = Math.cos(firstMode * u) * Math.cos(secondMode * v); + double second = Math.cos(secondMode * u) * Math.cos(firstMode * v); + double field = first * Math.cos(y * Math.PI * 0.5D) + - second * Math.cos(y * Math.PI * 0.75D); + double node = Math.max(0D, 1D - Math.abs(field) / width); + return node * node * (3D - 2D * node); + } + + 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/CircuitNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/CircuitNoise.java new file mode 100644 index 000000000..978ef670a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/CircuitNoise.java @@ -0,0 +1,73 @@ +package art.arcane.iris.util.project.noise; + +public final class CircuitNoise extends PatternNoise { + private final long seed; + private final double phaseX; + private final double phaseZ; + + public CircuitNoise(long seed) { + this.seed = seed; + phaseX = (mix(seed) >>> 11) * 0x1.0p-53; + phaseZ = (mix(seed + 0x9E3779B97F4A7C15L) >>> 11) * 0x1.0p-53; + } + + @Override + protected double sample(double x, double y, double z) { + x += phaseX; + z += phaseZ; + if (y != 0D) { + x += y * 0.13D + 0.17D * Math.sin(y * Math.PI * 0.5D); + z += 0.11D * Math.sin(y * Math.PI * 0.75D); + } + long cellX = (long) Math.floor(x); + long cellZ = (long) Math.floor(z); + x -= cellX; + z -= cellZ; + long hash = mix(seed ^ cellX * 0x9E3779B97F4A7C15L ^ cellZ * 0xD1B54A32D192ED03L); + double distance; + double padDistance; + if ((hash & 3L) == 0L) { + distance = Math.min(Math.abs(x - 0.5D), Math.abs(z - 0.5D)); + double dx = x - 0.5D; + double dz = z - 0.5D; + padDistance = Math.sqrt(dx * dx + dz * dz); + } else { + if ((hash & 1L) != 0L) { + x = 1D - x; + } + distance = Math.sqrt(Math.min(routeDistanceSquared(x, z), routeDistanceSquared(1D - x, 1D - z))); + double dx = x - 0.75D; + double dz = z - 0.25D; + double oppositeX = x - 0.25D; + double oppositeZ = z - 0.75D; + padDistance = Math.sqrt(Math.min(dx * dx + dz * dz, oppositeX * oppositeX + oppositeZ * oppositeZ)); + } + double trace = smooth(1D - distance / 0.045D); + double pad = smooth(1D - Math.abs(padDistance - 0.085D) / 0.03D); + return Math.max(trace, pad) * smooth(padDistance / 0.045D); + } + + private static double routeDistanceSquared(double x, double z) { + double top = segmentDistanceSquared(x - 0.5D, z, 0D, 0.25D); + double bend = segmentDistanceSquared(z - 0.25D, x, 0.5D, 0.75D); + double middle = segmentDistanceSquared(x - 0.75D, z, 0.25D, 0.5D); + double right = segmentDistanceSquared(z - 0.5D, x, 0.75D, 1D); + return Math.min(Math.min(top, bend), Math.min(middle, right)); + } + + private static double segmentDistanceSquared(double perpendicular, double along, double start, double end) { + double excess = Math.max(start - along, Math.max(along - end, 0D)); + return perpendicular * perpendicular + excess * excess; + } + + private static double smooth(double value) { + double bounded = Math.clamp(value, 0D, 1D); + return bounded * bounded * (3D - 2D * bounded); + } + + 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/DuneNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/DuneNoise.java new file mode 100644 index 000000000..2472da3c4 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/DuneNoise.java @@ -0,0 +1,61 @@ +package art.arcane.iris.util.project.noise; + +public final class DuneNoise extends PatternNoise { + private static final double TAU = Math.PI * 2D; + + private final long seed; + private final double cosine; + private final double sine; + private final double phase; + + public DuneNoise(long seed) { + this.seed = seed; + phase = unit(mix(seed)) * TAU; + cosine = Math.cos(phase); + sine = Math.sin(phase); + } + + @Override + protected double sample(double x, double y, double z) { + double wind = x * cosine + z * sine + y * 0.17D; + double across = z * cosine - x * sine + 0.12D * Math.sin(y * 0.7D + phase); + long cellWind = (long) Math.floor(wind); + long cellAcross = (long) Math.floor(across); + double dune = 0D; + for (int offsetWind = -1; offsetWind <= 1; offsetWind++) { + long gridWind = cellWind + offsetWind; + for (int offsetAcross = -1; offsetAcross <= 1; offsetAcross++) { + long gridAcross = cellAcross + offsetAcross; + long hash = mix(seed ^ gridWind * 0x9E3779B97F4A7C15L ^ gridAcross * 0xD1B54A32D192ED03L); + double centerWind = gridWind + 0.2D + (hash & 65535L) * (0.6D / 65535D); + double centerAcross = gridAcross + 0.2D + ((hash >>> 16) & 65535L) * (0.6D / 65535D); + double width = 0.32D + ((hash >>> 32) & 255L) * (0.16D / 255D); + double transverse = (across - centerAcross) / width; + double transverseSquared = transverse * transverse; + if (transverseSquared >= 1D) { + continue; + } + double crest = centerWind + 0.43D * transverseSquared; + double offset = wind - crest; + double windward = 0.52D + ((hash >>> 40) & 255L) * (0.14D / 255D); + double leeward = 0.1D + ((hash >>> 48) & 255L) * (0.035D / 255D); + double slope = Math.max(0D, 1D - Math.abs(offset) / (offset < 0D ? windward : leeward)); + double horns = 1D - transverseSquared * transverseSquared; + double height = 0.62D + (hash >>> 56) * (0.38D / 255D); + dune = Math.max(dune, height * horns * horns * slope * slope * (3D - 2D * slope)); + } + } + double ripples = 0.04D + 0.035D * Math.cos(wind * TAU * 2D + 0.5D * Math.sin(across * 1.7D + phase)); + return ripples + (1D - ripples) * dune; + } + + 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-53D; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/GaborNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/GaborNoise.java new file mode 100644 index 000000000..2fd5e7bc9 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/GaborNoise.java @@ -0,0 +1,72 @@ +package art.arcane.iris.util.project.noise; + +import java.util.SplittableRandom; + +public final class GaborNoise extends PatternNoise { + private static final double TAU = Math.PI * 2D; + private static final double RADIUS_SQUARED = 0.95D * 0.95D; + private static final double SPREAD = 0.6D / 65535D; + + private final long seed; + private final double directionX; + private final double directionY; + private final double directionZ; + + public GaborNoise(long seed) { + this.seed = seed; + SplittableRandom random = new SplittableRandom(seed); + double angle = random.nextDouble(TAU); + double elevation = random.nextDouble(-0.55D, 0.55D); + double horizontal = Math.sqrt(1D - elevation * elevation); + directionX = Math.cos(angle) * horizontal; + directionY = elevation; + directionZ = Math.sin(angle) * horizontal; + } + + @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 sum = 0D; + double weights = 0.12D; + 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); + if ((hash & 3L) == 0L) { + continue; + } + double dx = x - (pointX + 0.2D + (hash & 65535L) * SPREAD); + double dy = y - (pointY + 0.2D + ((hash >>> 16) & 65535L) * SPREAD); + double dz = z - (pointZ + 0.2D + ((hash >>> 32) & 65535L) * SPREAD); + double distanceSquared = dx * dx + dy * dy + dz * dz; + if (distanceSquared >= RADIUS_SQUARED) { + continue; + } + double envelope = 1D - distanceSquared / RADIUS_SQUARED; + double weight = envelope * envelope * envelope; + long wave = mix(hash); + double waveX = directionX + ((wave & 1023L) / 1023D - 0.5D) * 0.35D; + double waveY = directionY + (((wave >>> 10) & 1023L) / 1023D - 0.5D) * 0.35D; + double waveZ = directionZ + (((wave >>> 20) & 1023L) / 1023D - 0.5D) * 0.35D; + double phase = (wave >>> 32) * (TAU / 4294967296D); + sum += weight * Math.cos(TAU * 3D * (waveX * dx + waveY * dy + waveZ * dz) + phase); + weights += weight; + } + } + } + return 0.5D + 0.5D * sum / weights; + } + + 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/KaleidoscopeNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/KaleidoscopeNoise.java new file mode 100644 index 000000000..3bde2b2ad --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/KaleidoscopeNoise.java @@ -0,0 +1,43 @@ +package art.arcane.iris.util.project.noise; + +public final class KaleidoscopeNoise extends PatternNoise { + private static final double TAU = Math.PI * 2D; + + private final long seed; + + public KaleidoscopeNoise(long seed) { + this.seed = seed; + } + + @Override + protected double sample(double x, double y, double z) { + z += 0.5D; + long cellX = (long) Math.floor(x); + long cellZ = (long) Math.floor(z); + double dx = x - cellX - 0.5D; + double dz = z - cellZ - 0.5D; + double radiusSquared = dx * dx + dz * dz; + if (radiusSquared >= 0.25D) { + return 0D; + } + + long hash = mix(seed ^ cellX * 0x9E3779B97F4A7C15L ^ cellZ * 0xD1B54A32D192ED03L); + int wedges = 4 + (int) (hash & 3L); + double phase = (hash >>> 11) * 0x1.0p-53D * TAU; + double sector = TAU / wedges; + double angle = Math.atan2(dz, dx) + phase + y * 0.35D; + angle = Math.abs(angle - Math.floor(angle / sector + 0.5D) * sector); + double radius = Math.sqrt(radiusSquared); + double u = radius * Math.cos(angle); + double v = radius * Math.sin(angle); + double motif = 0.5D + 0.5D * Math.cos(16D * u + 2.5D * Math.cos(18D * v + phase + y)); + double envelope = 1D - radiusSquared * 4D; + return motif * envelope * envelope; + } + + 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/MarbleNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/MarbleNoise.java new file mode 100644 index 000000000..7d58edd2c --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/MarbleNoise.java @@ -0,0 +1,36 @@ +package art.arcane.iris.util.project.noise; + +import java.util.SplittableRandom; + +public final class MarbleNoise extends PatternNoise { + private static final double TAU = Math.PI * 2D; + + private final FastNoiseDouble distortion; + private final double directionX; + private final double directionY; + private final double directionZ; + private final double phase; + + public MarbleNoise(long seed) { + SplittableRandom random = new SplittableRandom(seed); + distortion = new FastNoiseDouble(random.nextLong()); + distortion.setFrequency(1D); + double angle = random.nextDouble(TAU); + directionX = Math.cos(angle); + directionY = random.nextDouble(0.25D, 0.65D); + directionZ = Math.sin(angle); + phase = random.nextDouble(TAU); + } + + @Override + protected double sample(double x, double y, double z) { + double broad = distortion.GetSimplex(x * 0.45D, y * 0.45D, z * 0.45D); + double detail = distortion.GetSimplex(x * 1.3D + 17.3D, y * 1.3D - 11.7D, z * 1.3D + 5.9D); + double warp = broad * 1.1D + detail * 0.2D; + double position = x * directionX + y * directionY + z * directionZ; + double ridge = Math.sin(TAU * (position + warp) + phase); + double vein = 1D / (1D + 90D * ridge * ridge); + double stone = 0.85D + 0.15D * broad; + return Math.clamp(0.1D + 0.84D * stone * (1D - vein), 0D, 1D); + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/MengerSpongeNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/MengerSpongeNoise.java new file mode 100644 index 000000000..4505dc21a --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/MengerSpongeNoise.java @@ -0,0 +1,48 @@ +package art.arcane.iris.util.project.noise; + +public final class MengerSpongeNoise extends PatternNoise { + private static final int DEPTH = 3; + + private final double phaseX; + private final double phaseY; + private final double phaseZ; + + public MengerSpongeNoise(long seed) { + long hash = mix(seed); + phaseX = (hash & 0x1FFFFFL) * 0x1.0p-21D; + phaseY = ((hash >>> 21) & 0x1FFFFFL) * (0x1.0p-21D / 3D); + phaseZ = (hash >>> 42) * 0x1.0p-22D; + } + + @Override + protected double sample(double x, double y, double z) { + x += phaseX; + y += phaseY; + z += phaseZ; + x -= Math.floor(x); + y -= Math.floor(y); + z -= Math.floor(z); + for (int level = 0; level < DEPTH; level++) { + x *= 3D; + y *= 3D; + z *= 3D; + int cellX = (int) x; + int cellY = (int) y; + int cellZ = (int) z; + int middleAxes = (cellX == 1 ? 1 : 0) + (cellY == 1 ? 1 : 0) + (cellZ == 1 ? 1 : 0); + if (middleAxes >= 2) { + return 0D; + } + x -= cellX; + y -= cellY; + z -= cellZ; + } + return 1D; + } + + 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/NoiseType.java b/core/src/main/java/art/arcane/iris/util/project/noise/NoiseType.java index 78c6e0079..7c3867bdd 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 @@ -92,7 +92,17 @@ public enum NoiseType { QUASICRYSTAL(QuasicrystalNoise::new, 1D / 64D), TRUCHET(TruchetNoise::new, 1D / 64D), CRATER(CraterNoise::new, 1D / 64D), - VORTEX(VortexNoise::new, 1D / 64D); + VORTEX(VortexNoise::new, 1D / 64D), + DUNE(DuneNoise::new, 1D / 64D), + STRATA(StrataNoise::new, 1D / 64D), + WOOD(WoodNoise::new, 1D / 64D), + GABOR(GaborNoise::new, 1D / 64D), + MARBLE(MarbleNoise::new, 1D / 64D), + SCALES(ScalesNoise::new, 1D / 64D), + CHLADNI(ChladniNoise::new, 1D / 64D), + KALEIDOSCOPE(KaleidoscopeNoise::new, 1D / 64D), + MENGER_SPONGE(MengerSpongeNoise::new, 1D / 64D), + CIRCUIT(CircuitNoise::new, 1D / 64D); private final NoiseFactory f; private final double coordinateScale; diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/ScalesNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/ScalesNoise.java new file mode 100644 index 000000000..053a6cfc2 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/ScalesNoise.java @@ -0,0 +1,61 @@ +package art.arcane.iris.util.project.noise; + +public final class ScalesNoise extends PatternNoise { + private static final double TAU = Math.PI * 2D; + private static final double RADIUS = 0.5D; + private static final double ROW_HEIGHT = 0.5D; + private static final double GROOVE_WIDTH = 0.055D; + + private final long seed; + private final double phaseX; + private final double phaseZ; + + public ScalesNoise(long seed) { + this.seed = seed; + phaseX = (mix(seed) >>> 11) * 0x1.0p-53D; + phaseZ = (mix(seed + 0x9E3779B97F4A7C15L) >>> 11) * 0x1.0p-53D; + } + + @Override + protected double sample(double x, double y, double z) { + x += phaseX + Math.sin(y * TAU * 0.5D) * 0.22D + y * 0.15D; + z += phaseZ + Math.sin(y * TAU * 0.25D) * 0.12D + y * 0.2D; + long row = (long) Math.floor(z / ROW_HEIGHT); + double groove = 0D; + double shade = 0D; + for (int offsetZ = -1; offsetZ <= 1; offsetZ++) { + long pointZ = row + offsetZ; + double shift = (pointZ & 1L) * 0.5D; + long column = (long) Math.floor(x - shift); + double dz = z - pointZ * ROW_HEIGHT; + for (int offsetX = -1; offsetX <= 1; offsetX++) { + long pointX = column + offsetX; + double dx = x - (pointX + shift); + double radiusSquared = dx * dx + dz * dz; + double distance; + if (dz >= 0D) { + distance = Math.abs(Math.sqrt(radiusSquared) - RADIUS); + } else { + double endpoint = Math.abs(dx) - RADIUS; + distance = Math.sqrt(endpoint * endpoint + dz * dz); + } + double rim = Math.max(0D, 1D - distance / GROOVE_WIDTH); + if (rim > 0D || radiusSquared < RADIUS * RADIUS && dz > 0D) { + long hash = mix(seed ^ pointX * 0x9E3779B97F4A7C15L ^ pointZ * 0xC2B2AE3D27D4EB4FL); + double variation = 0.75D + (hash >>> 11) * 0x1.0p-53D * 0.25D; + groove = Math.max(groove, rim * rim * (3D - 2D * rim) * variation); + double inside = Math.max(0D, 1D - radiusSquared / (RADIUS * RADIUS)); + double rise = Math.clamp(dz / 0.12D, 0D, 1D); + shade = Math.max(shade, inside * rise * rise * (3D - 2D * rise) * variation); + } + } + } + return 0.72D + 0.22D * shade - 0.65D * groove; + } + + 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/StrataNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/StrataNoise.java new file mode 100644 index 000000000..b5ccec2ae --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/StrataNoise.java @@ -0,0 +1,37 @@ +package art.arcane.iris.util.project.noise; + +import java.util.SplittableRandom; + +public final class StrataNoise extends PatternNoise { + private static final double TAU = Math.PI * 2D; + + private final double cosine; + private final double sine; + private final double phaseA; + private final double phaseB; + private final double phaseC; + + public StrataNoise(long seed) { + SplittableRandom random = new SplittableRandom(seed); + double angle = random.nextDouble(TAU); + cosine = Math.cos(angle); + sine = Math.sin(angle); + phaseA = random.nextDouble(TAU); + phaseB = random.nextDouble(TAU); + phaseC = random.nextDouble(TAU); + } + + @Override + protected double sample(double x, double y, double z) { + double along = x * cosine + z * sine; + double across = z * cosine - x * sine; + double fold = 1.1D * Math.sin(along * 1.1D + phaseA) + + 0.35D * Math.sin(along * 2.7D + across * 0.8D + phaseB + y * 0.4D) + + 0.2D * Math.sin(across * 1.7D + y * 0.9D + phaseC); + double layer = TAU * (across + y * 0.75D + fold); + double thickness = layer + 0.55D * Math.sin(layer * 0.25D + phaseB); + double broadBand = 0.5D + 0.5D * Math.sin(thickness); + double lamina = 0.5D + 0.5D * Math.sin(thickness * 3D + phaseC); + return 0.82D * broadBand + 0.18D * lamina; + } +} diff --git a/core/src/main/java/art/arcane/iris/util/project/noise/WoodNoise.java b/core/src/main/java/art/arcane/iris/util/project/noise/WoodNoise.java new file mode 100644 index 000000000..4791f0822 --- /dev/null +++ b/core/src/main/java/art/arcane/iris/util/project/noise/WoodNoise.java @@ -0,0 +1,56 @@ +package art.arcane.iris.util.project.noise; + +public final class WoodNoise extends PatternNoise { + private static final double TAU = Math.PI * 2D; + + private final long seed; + private final double cosine; + private final double sine; + private final double phase; + + public WoodNoise(long seed) { + this.seed = seed; + phase = (mix(seed) >>> 11) * 0x1.0p-53D * TAU; + cosine = Math.cos(phase); + sine = Math.sin(phase); + } + + @Override + protected double sample(double x, double y, double z) { + double along = (z * cosine - x * sine) * 0.45D + y * 0.17D; + double across = x * cosine + z * sine + y * 0.07D + 0.11D * Math.sin(along * 0.85D + phase); + double grainPhase = across * TAU * 3D + 0.18D * Math.sin(along * 2.1D + phase); + double value = 0.12D * (0.5D + 0.5D * Math.sin(grainPhase)); + double weightSum = 0.12D; + long cellAcross = (long) Math.floor(across); + long cellAlong = (long) Math.floor(along); + for (int offsetAcross = -1; offsetAcross <= 1; offsetAcross++) { + long gridAcross = cellAcross + offsetAcross; + for (int offsetAlong = -1; offsetAlong <= 1; offsetAlong++) { + long gridAlong = cellAlong + offsetAlong; + long hash = mix(seed ^ gridAcross * 0x9E3779B97F4A7C15L ^ gridAlong * 0xD1B54A32D192ED03L); + double deltaAcross = across - (gridAcross + 0.3D + (hash & 65535L) * (0.4D / 65535D)); + double deltaAlong = along - (gridAlong + 0.3D + ((hash >>> 16) & 65535L) * (0.4D / 65535D)); + double radius = 0.5D + ((hash >>> 32) & 255L) * (0.12D / 255D); + double distanceSquared = deltaAcross * deltaAcross + deltaAlong * deltaAlong; + double envelope = 1D - distanceSquared / (radius * radius); + if (envelope <= 0D) { + continue; + } + double rings = Math.sqrt(distanceSquared + 0.0004D) * TAU * 5D; + double ringPhase = ((hash >>> 40) & 65535L) * (TAU / 65535D); + double knot = 0.5D + 0.5D * Math.sin(rings + ringPhase); + double weight = 3D * envelope * envelope; + value += knot * weight; + weightSum += weight; + } + } + return value / weightSum; + } + + 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/test/java/art/arcane/iris/util/project/noise/ChladniNoiseTest.java b/core/src/test/java/art/arcane/iris/util/project/noise/ChladniNoiseTest.java new file mode 100644 index 000000000..ac45b977f --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/noise/ChladniNoiseTest.java @@ -0,0 +1,69 @@ +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 ChladniNoiseTest { + @Test + public void horizontalPlateHasMirroredNodalLinesAndDarkAntinodes() { + for (long seed : new long[]{0L, 1337L, Long.MIN_VALUE}) { + ChladniNoise noise = new ChladniNoise(seed); + double minimum = 1D; + for (int index = 0; index < 256; index++) { + double x = index * 0.031D - 4D; + double z = index * -0.047D + 3D; + assertEquals(1D, noise.noise(x, x), 0D); + assertEquals(noise.noise(x, z), noise.noise(z, x), 1.0E-12D); + minimum = Math.min(minimum, noise.noise(x, z)); + } + assertTrue(minimum < 0.01D); + } + } + + @Test + public void standingModesChangeContinuouslyWithHeight() { + ChladniNoise noise = new ChladniNoise(1337L); + double variation = 0D; + for (int index = 0; index < 512; index++) { + double x = index * 0.019D - 5D; + double z = index * -0.023D + 3D; + assertEquals(noise.noise(x, z), noise.noise(x, 0D, z), 0D); + for (double y : new double[]{-2D, 0D, 1D, 468750D}) { + assertEquals(noise.noise(x, y - 1.0E-8D, z), + noise.noise(x, y + 1.0E-8D, z), 1.0E-5D); + } + variation += Math.abs(noise.noise(x, z) - noise.noise(x, 0.7D, z)); + } + assertTrue(variation > 20D); + } + + @Test + public void upperSeedBitsChangeThePlate() { + ChladniNoise first = new ChladniNoise(1337L); + ChladniNoise second = new ChladniNoise(1337L + (1L << 32)); + double variation = 0D; + for (int index = 0; index < 256; index++) { + variation += Math.abs(first.noise(index * 0.019D) - second.noise(index * 0.019D)); + } + assertTrue(variation > 10D); + } + + @Test + public void octaveSamplesStayBoundedAndRepeatableAtWorldBorders() { + ChladniNoise first = new ChladniNoise(Long.MAX_VALUE); + ChladniNoise second = new ChladniNoise(Long.MAX_VALUE); + for (int octaves : new int[]{1, 3, 16}) { + first.setOctaves(octaves); + second.setOctaves(octaves); + for (int index = 0; index < 128; index++) { + double x = -468750D + index * (937500D / 127D); + double value = first.noise(x, x * 0.37D, x * -0.71D); + assertTrue(Double.isFinite(value)); + assertTrue(value >= 0D && value <= 1D); + assertEquals(value, second.noise(x, x * 0.37D, x * -0.71D), 0D); + } + } + } +} diff --git a/core/src/test/java/art/arcane/iris/util/project/noise/CircuitNoiseTest.java b/core/src/test/java/art/arcane/iris/util/project/noise/CircuitNoiseTest.java new file mode 100644 index 000000000..b7ba068dc --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/noise/CircuitNoiseTest.java @@ -0,0 +1,45 @@ +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 CircuitNoiseTest { + @Test + public void tracksMeetAtTilePortsWithoutBoundaryJumps() { + CircuitNoise noise = new CircuitNoise(0L); + double maximum = 0D; + for (int boundary = -8; boundary <= 8; boundary++) { + for (int step = 0; step < 256; step++) { + double z = step * 0.019D - 2D; + assertEquals(noise.noise(boundary - 1E-8D, z), noise.noise(boundary + 1E-8D, z), 1E-5D); + maximum = Math.max(maximum, noise.noise(boundary, z)); + } + } + assertTrue(maximum > 0.99D); + } + + @Test + public void circuitHasOpenBoardTracesAndIntermediateEdges() { + CircuitNoise noise = new CircuitNoise(1337L); + int open = 0; + int trace = 0; + int edge = 0; + for (int x = 0; x < 256; x++) { + for (int z = 0; z < 256; z++) { + double value = noise.noise(x / 64D, z / 64D); + if (value == 0D) { + open++; + } else if (value > 0.8D) { + trace++; + } else { + edge++; + } + } + } + assertTrue(open > 32768); + assertTrue(trace > 1500); + assertTrue(edge > 1500); + } +} diff --git a/core/src/test/java/art/arcane/iris/util/project/noise/GaborNoiseTest.java b/core/src/test/java/art/arcane/iris/util/project/noise/GaborNoiseTest.java new file mode 100644 index 000000000..c0d48f2d4 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/noise/GaborNoiseTest.java @@ -0,0 +1,40 @@ +package art.arcane.iris.util.project.noise; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public class GaborNoiseTest { + @Test + public void wavePacketsHaveDirectionalDetailAndQuietGaps() { + GaborNoise noise = new GaborNoise(1337L); + double xx = 0D; + double xz = 0D; + double zz = 0D; + int quiet = 0; + int dark = 0; + int light = 0; + for (int row = 0; row < 128; row++) { + for (int column = 0; column < 128; column++) { + double x = column * 0.0625D - 4D; + double z = row * 0.0625D - 4D; + double value = noise.noise(x, z); + quiet += Math.abs(value - 0.5D) < 0.03D ? 1 : 0; + dark += value < 0.3D ? 1 : 0; + light += value > 0.7D ? 1 : 0; + double dx = noise.noise(x + 0.001D, z) - noise.noise(x - 0.001D, z); + double dz = noise.noise(x, z + 0.001D) - noise.noise(x, z - 0.001D); + xx += dx * dx; + xz += dx * dz; + zz += dz * dz; + } + } + double spread = Math.sqrt((xx - zz) * (xx - zz) + 4D * xz * xz); + double major = xx + zz + spread; + double minor = xx + zz - spread; + assertTrue(major > minor * 3D); + assertTrue(quiet > 1000); + assertTrue(dark > 500); + assertTrue(light > 500); + } +} diff --git a/core/src/test/java/art/arcane/iris/util/project/noise/KaleidoscopeNoiseTest.java b/core/src/test/java/art/arcane/iris/util/project/noise/KaleidoscopeNoiseTest.java new file mode 100644 index 000000000..68683c3c1 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/noise/KaleidoscopeNoiseTest.java @@ -0,0 +1,92 @@ +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 KaleidoscopeNoiseTest { + @Test + public void wedgesMirrorAndRepeatAroundTheTileCenter() { + KaleidoscopeNoise noise = new KaleidoscopeNoise(0L); + for (int index = 0; index < 128; index++) { + double angle = index * 0.037D; + double radius = 0.05D + index * (0.4D / 128D); + double x = radius * Math.cos(angle); + double z = radius * Math.sin(angle); + double value = noise.noise(0.5D + x, z); + assertEquals(value, noise.noise(0.5D + x, -z), 1.0E-12D); + assertEquals(value, noise.noise(0.5D - z, x), 1.0E-12D); + } + } + + @Test + public void compactMotifsMeetContinuouslyAtTileAndWedgeBoundaries() { + double epsilon = 1.0E-8D; + for (long seed : new long[]{0L, 1337L, Long.MIN_VALUE}) { + KaleidoscopeNoise noise = new KaleidoscopeNoise(seed); + for (int index = 0; index < 256; index++) { + double position = index * 0.027D - 3D; + double height = index * 0.017D - 2D; + assertEquals(noise.noise(-epsilon, height, position), + noise.noise(epsilon, height, position), 1.0E-6D); + assertEquals(noise.noise(position, height, 0.5D - epsilon), + noise.noise(position, height, 0.5D + epsilon), 1.0E-6D); + assertEquals(noise.noise(0.5D - epsilon, height, 0D), + noise.noise(0.5D + epsilon, height, 0D), 1.0E-5D); + } + } + KaleidoscopeNoise noise = new KaleidoscopeNoise(0L); + for (double boundary : new double[]{-Math.PI, -Math.PI * 0.75D, 0D, Math.PI * 0.25D, Math.PI}) { + assertEquals(noise.noise(0.5D + 0.3D * Math.cos(boundary - epsilon), + 0.3D * Math.sin(boundary - epsilon)), + noise.noise(0.5D + 0.3D * Math.cos(boundary + epsilon), + 0.3D * Math.sin(boundary + epsilon)), 1.0E-5D); + } + } + + @Test + public void heightAndUpperSeedBitsChangeTheMotifs() { + KaleidoscopeNoise first = new KaleidoscopeNoise(1337L); + KaleidoscopeNoise second = new KaleidoscopeNoise(1337L + (1L << 32)); + double heightVariation = 0D; + double seedVariation = 0D; + double maximum = 0D; + for (int index = 0; index < 512; index++) { + double x = index * 0.037D - 5D; + double z = index * -0.023D + 3D; + double value = first.noise(x, z); + assertEquals(value, first.noise(x, 0D, z), 0D); + assertEquals(value, first.noise(x, 1.0E-8D, z), 1.0E-5D); + heightVariation += Math.abs(value - first.noise(x, 1.25D, z)); + seedVariation += Math.abs(value - second.noise(x, z)); + maximum = Math.max(maximum, value); + } + assertTrue(heightVariation > 10D); + assertTrue(seedVariation > 10D); + assertTrue(maximum > 0.7D); + double oneDimensionalVariation = 0D; + for (int index = 0; index < 256; index++) { + oneDimensionalVariation += Math.abs(first.noise(index * 0.013D) + - second.noise(index * 0.013D)); + } + assertTrue(oneDimensionalVariation > 10D); + } + + @Test + public void octaveSamplesStayBoundedAndRepeatableAtWorldBorders() { + KaleidoscopeNoise first = new KaleidoscopeNoise(Long.MAX_VALUE); + KaleidoscopeNoise second = new KaleidoscopeNoise(Long.MAX_VALUE); + for (int octaves : new int[]{1, 3, 16}) { + first.setOctaves(octaves); + second.setOctaves(octaves); + for (int index = 0; index < 128; index++) { + double x = -468750D + index * (937500D / 127D); + double value = first.noise(x, x * 0.37D, x * -0.71D); + assertTrue(Double.isFinite(value)); + assertTrue(value >= 0D && value <= 1D); + assertEquals(value, second.noise(x, x * 0.37D, x * -0.71D), 0D); + } + } + } +} diff --git a/core/src/test/java/art/arcane/iris/util/project/noise/MarbleNoiseTest.java b/core/src/test/java/art/arcane/iris/util/project/noise/MarbleNoiseTest.java new file mode 100644 index 000000000..295592d31 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/noise/MarbleNoiseTest.java @@ -0,0 +1,29 @@ +package art.arcane.iris.util.project.noise; + +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public class MarbleNoiseTest { + @Test + public void narrowVeinsCrossPredominantlySoftStone() { + MarbleNoise noise = new MarbleNoise(1337L); + int dark = 0; + int soft = 0; + double minimum = 1D; + double maximum = 0D; + for (int row = 0; row < 128; row++) { + for (int column = 0; column < 128; column++) { + double value = noise.noise(column * 0.0625D - 4D, row * 0.0625D - 4D); + dark += value < 0.3D ? 1 : 0; + soft += value > 0.65D ? 1 : 0; + minimum = Math.min(minimum, value); + maximum = Math.max(maximum, value); + } + } + assertTrue(minimum < 0.15D); + assertTrue(maximum > 0.85D); + assertTrue(dark > 250 && dark < 2500); + assertTrue(soft > 10000); + } +} diff --git a/core/src/test/java/art/arcane/iris/util/project/noise/MaterialPatternNoiseTest.java b/core/src/test/java/art/arcane/iris/util/project/noise/MaterialPatternNoiseTest.java new file mode 100644 index 000000000..046fcb515 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/noise/MaterialPatternNoiseTest.java @@ -0,0 +1,114 @@ +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 MaterialPatternNoiseTest { + @Test + public void materialPatternsStayBoundedSeededAndVolumetric() { + for (long seed : new long[]{0L, 813753L, Long.MIN_VALUE, Long.MAX_VALUE}) { + PatternNoise[] patterns = patterns(seed); + PatternNoise[] repeated = patterns(seed); + PatternNoise[] otherSeed = patterns(seed ^ (1L << 47)); + for (int pattern = 0; pattern < patterns.length; pattern++) { + PatternNoise noise = patterns[pattern]; + double seedDifference = 0D; + double heightDifference = 0D; + for (int octaves : new int[]{1, 4, 16}) { + noise.setOctaves(octaves); + repeated[pattern].setOctaves(octaves); + otherSeed[pattern].setOctaves(octaves); + for (double origin : new double[]{-468750D, 0D, 468750D}) { + for (int i = 0; i < 128; i++) { + double x = origin + i * 0.173D; + double y = i * 0.071D - 4D; + double z = -origin + i * 0.319D; + double value = noise.noise(x, y, z); + assertTrue(Double.isFinite(value) && value >= 0D && value <= 1D); + assertEquals(value, repeated[pattern].noise(x, y, z), 0D); + assertEquals(noise.noise(x, z), noise.noise(x, 0D, z), 0D); + seedDifference += Math.abs(value - otherSeed[pattern].noise(x, y, z)); + heightDifference += Math.abs(value - noise.noise(x, y + 0.713D, z)); + } + } + } + assertTrue(noise.getClass().getSimpleName(), seedDifference > 10D); + assertTrue(noise.getClass().getSimpleName(), heightDifference > 10D); + } + } + } + + @Test + public void materialPatternsHaveNoCoordinateBoundaryJumps() { + for (long seed : new long[]{0L, 813753L, Long.MAX_VALUE}) { + for (PatternNoise noise : patterns(seed)) { + for (double boundary : new double[]{-468750D, -2D, 0D, 3D, 468750D}) { + for (int i = 0; i < 128; i++) { + double coordinate = i * 0.073D - 4D; + assertEquals(noise.noise(boundary - 1E-7D, 0.317D, coordinate), + noise.noise(boundary + 1E-7D, 0.317D, coordinate), 1E-4D); + assertEquals(noise.noise(coordinate, 0.317D, boundary - 1E-7D), + noise.noise(coordinate, 0.317D, boundary + 1E-7D), 1E-4D); + } + } + } + } + } + + @Test + public void duneWindwardSlopeIsLongerAndHornsBendDownwind() { + DuneNoise dune = new DuneNoise(0L); + assertTrue(dune.noise(0.02D, 0.2D) > dune.noise(0.38D, 0.2D) + 0.2D); + double across = 0.2D + 0.32D * 0.65D; + double crest = 0.2D + 0.43D * 0.65D * 0.65D; + assertTrue(dune.noise(crest, across) > dune.noise(0.2D, across) + 0.05D); + assertTrue(dune.noise(0.2D, 0.2D) > dune.noise(crest, across)); + } + + @Test + public void woodKnotsCarryClosedGrowthRings() { + WoodNoise wood = new WoodNoise(0L); + double minimum = 1D; + double maximum = 0D; + for (int i = 0; i < 64; i++) { + double angle = i * Math.PI / 32D; + double across = 0.3D + Math.cos(angle) * 0.12D; + double along = 0.3D + Math.sin(angle) * 0.12D; + double x = across - 0.11D * Math.sin(along * 0.85D); + double value = wood.noise(x, along / 0.45D); + minimum = Math.min(minimum, value); + maximum = Math.max(maximum, value); + } + double brightRingX = 0.34D - 0.11D * Math.sin(0.3D * 0.85D); + assertTrue(maximum - minimum < 0.22D); + assertTrue(wood.noise(brightRingX, 0.3D / 0.45D) > maximum + 0.4D); + } + + @Test + public void strataContainsRepeatedHighAndLowBands() { + StrataNoise strata = new StrataNoise(813753L); + int highBands = 0; + int lowBands = 0; + double previous = strata.noise(0D, -8D); + double current = strata.noise(0D, -8D + 0.008D); + for (int i = 2; i < 2048; i++) { + double next = strata.noise(0D, -8D + i * 0.008D); + if (current > previous && current > next && current > 0.75D) { + highBands++; + } + if (current < previous && current < next && current < 0.25D) { + lowBands++; + } + previous = current; + current = next; + } + assertTrue(highBands >= 6); + assertTrue(lowBands >= 6); + } + + private static PatternNoise[] patterns(long seed) { + return new PatternNoise[]{new DuneNoise(seed), new StrataNoise(seed), new WoodNoise(seed)}; + } +} diff --git a/core/src/test/java/art/arcane/iris/util/project/noise/MengerSpongeNoiseTest.java b/core/src/test/java/art/arcane/iris/util/project/noise/MengerSpongeNoiseTest.java new file mode 100644 index 000000000..86f6a634d --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/noise/MengerSpongeNoiseTest.java @@ -0,0 +1,83 @@ +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 MengerSpongeNoiseTest { + @Test + public void threeSubdivisionsRetainExactlyTwentyCubedCells() { + MengerSpongeNoise noise = new MengerSpongeNoise(0L); + int retained = 0; + for (int x = 0; x < 27; x++) { + for (int y = 0; y < 27; y++) { + for (int z = 0; z < 27; z++) { + retained += (int) noise.noise((x + 0.5D) / 27D, + (y + 0.5D) / 27D, (z + 0.5D) / 27D); + } + } + } + assertEquals(8000, retained); + } + + @Test + public void faceSliceContainsThreeLevelsOfSquareHoles() { + MengerSpongeNoise noise = new MengerSpongeNoise(0L); + assertEquals(0D, noise.noise(0.5D, 0D, 0.5D), 0D); + assertEquals(0D, noise.noise(1D / 6D, 0D, 1D / 6D), 0D); + assertEquals(0D, noise.noise(1D / 18D, 0D, 1D / 18D), 0D); + assertEquals(1D, noise.noise(1D / 54D, 0D, 1D / 54D), 0D); + assertEquals(0D, noise.noise(0.5D, 0.5D, 0.05D), 0D); + assertEquals(0D, noise.noise(0.05D, 0.5D, 0.5D), 0D); + int retained = 0; + for (int x = 0; x < 27; x++) { + for (int z = 0; z < 27; z++) { + retained += (int) noise.noise((x + 0.5D) / 27D, (z + 0.5D) / 27D); + } + } + assertEquals(512, retained); + } + + @Test + public void negativeTilesRepeatAndAllAxesAndUpperSeedBitsAffectGeometry() { + MengerSpongeNoise first = new MengerSpongeNoise(1337L); + MengerSpongeNoise second = new MengerSpongeNoise(1337L + (1L << 32)); + double xVariation = 0D; + double yVariation = 0D; + double zVariation = 0D; + double seedVariation = 0D; + for (int index = 0; index < 512; index++) { + double x = index * 0.019D - 4D; + double z = index * -0.031D + 2D; + double value = first.noise(x, z); + assertEquals(value, first.noise(x, 0D, z), 0D); + assertEquals(value, first.noise(x - 2D, 0D, z + 3D), 0D); + xVariation += Math.abs(value - first.noise(x + 0.137D, z)); + yVariation += Math.abs(value - first.noise(x, 0.419D, z)); + zVariation += Math.abs(value - first.noise(x, z + 0.237D)); + seedVariation += Math.abs(value - second.noise(x, z)); + } + assertTrue(xVariation > 20D); + assertTrue(yVariation > 20D); + assertTrue(zVariation > 20D); + assertTrue(seedVariation > 20D); + } + + @Test + public void octaveSamplesStayBoundedAndRepeatableAtWorldBorders() { + MengerSpongeNoise first = new MengerSpongeNoise(Long.MAX_VALUE); + MengerSpongeNoise second = new MengerSpongeNoise(Long.MAX_VALUE); + for (int octaves : new int[]{1, 3, 16}) { + first.setOctaves(octaves); + second.setOctaves(octaves); + for (int index = 0; index < 128; index++) { + double x = -468750D + index * (937500D / 127D); + double value = first.noise(x, x * 0.37D, x * -0.71D); + assertTrue(Double.isFinite(value)); + assertTrue(value >= 0D && value <= 1D); + assertEquals(value, second.noise(x, x * 0.37D, x * -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 index e2e88dfbb..f6aee8719 100644 --- 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 @@ -12,7 +12,9 @@ import static org.junit.Assert.assertTrue; public class PatternNoiseTest { private static final NoiseStyle[] STYLES = { NoiseStyle.GYROID, NoiseStyle.QUASICRYSTAL, NoiseStyle.TRUCHET, - NoiseStyle.CRATER, NoiseStyle.VORTEX + NoiseStyle.CRATER, NoiseStyle.VORTEX, + NoiseStyle.DUNE, NoiseStyle.STRATA, NoiseStyle.WOOD, NoiseStyle.GABOR, NoiseStyle.MARBLE, + NoiseStyle.SCALES, NoiseStyle.CHLADNI, NoiseStyle.KALEIDOSCOPE, NoiseStyle.MENGER_SPONGE, NoiseStyle.CIRCUIT }; @Test @@ -100,7 +102,9 @@ public class PatternNoiseTest { private static PatternNoise[] patterns(long seed) { return new PatternNoise[]{new GyroidNoise(seed), new QuasicrystalNoise(seed), - new TruchetNoise(seed), new CraterNoise(seed), new VortexNoise(seed)}; + new TruchetNoise(seed), new CraterNoise(seed), new VortexNoise(seed), + new DuneNoise(seed), new StrataNoise(seed), new WoodNoise(seed), new GaborNoise(seed), new MarbleNoise(seed), + new ScalesNoise(seed), new ChladniNoise(seed), new KaleidoscopeNoise(seed), new MengerSpongeNoise(seed), new CircuitNoise(seed)}; } private static void assertUnit(PatternNoise noise, double value) { diff --git a/core/src/test/java/art/arcane/iris/util/project/noise/ScalesNoiseTest.java b/core/src/test/java/art/arcane/iris/util/project/noise/ScalesNoiseTest.java new file mode 100644 index 000000000..e2c34bac2 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/noise/ScalesNoiseTest.java @@ -0,0 +1,38 @@ +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 ScalesNoiseTest { + private static final double ZERO_SEED_PHASE_Z = 0.8833108082136426D; + + @Test + public void groovesTraceStaggeredSemicirclesAroundRaisedFaces() { + ScalesNoise noise = new ScalesNoise(0L); + for (int row = -3; row <= 3; row++) { + double x = (row & 1) * 0.5D; + double z = row * 0.5D - ZERO_SEED_PHASE_Z; + assertTrue(noise.noise(x, z + 0.2D) > 0.75D); + for (int point = 1; point < 12; point++) { + double angle = point * Math.PI / 12D; + assertTrue(noise.noise(x + Math.cos(angle) * 0.5D, z + Math.sin(angle) * 0.5D) < 0.24D); + } + } + } + + @Test + public void staggeredRowAndColumnBoundariesRemainContinuous() { + ScalesNoise noise = new ScalesNoise(0L); + for (int row = -4; row <= 4; row++) { + double z = row * 0.5D - ZERO_SEED_PHASE_Z; + for (int point = 0; point < 512; point++) { + double x = point / 128D - 2D; + assertEquals(noise.noise(x, z - 1.0E-8D), noise.noise(x, z + 1.0E-8D), 1.0E-5D); + assertEquals(noise.noise(row * 0.5D - 1.0E-8D, x), + noise.noise(row * 0.5D + 1.0E-8D, x), 1.0E-5D); + } + } + } +} diff --git a/core/src/test/java/art/arcane/iris/util/project/noise/TextureNoiseTest.java b/core/src/test/java/art/arcane/iris/util/project/noise/TextureNoiseTest.java new file mode 100644 index 000000000..6eeb91365 --- /dev/null +++ b/core/src/test/java/art/arcane/iris/util/project/noise/TextureNoiseTest.java @@ -0,0 +1,105 @@ +package art.arcane.iris.util.project.noise; + +import org.junit.Test; + +import java.util.SplittableRandom; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class TextureNoiseTest { + @Test + public void texturesKeepUnitRangeAndExactHorizontalSlicesThroughSixteenOctaves() { + SplittableRandom random = new SplittableRandom(715L); + for (long seed : new long[]{0L, 1337L, Long.MIN_VALUE, Long.MAX_VALUE}) { + for (PatternNoise noise : textures(seed)) { + for (int octaves : new int[]{1, 4, 16}) { + noise.setOctaves(octaves); + for (int index = 0; index < 128; index++) { + double x = random.nextDouble(-468750D, 468750D); + double y = random.nextDouble(-32D, 32D); + double z = random.nextDouble(-468750D, 468750D); + double value = noise.noise(x, y, z); + assertTrue(noise.getClass().getSimpleName(), Double.isFinite(value) && value >= 0D && value <= 1D); + assertEquals(noise.noise(x, z), noise.noise(x, 0D, z), 0D); + assertEquals(noise.noise(x), noise.noise(x, 0D, 0D), 0D); + } + } + } + } + } + + @Test + public void texturesUseTheFullSeedAndEveryCoordinate() { + PatternNoise[] first = textures(1337L); + PatternNoise[] same = textures(1337L); + PatternNoise[] highSeed = textures(1337L + (1L << 32)); + for (int style = 0; style < first.length; style++) { + double seedDifference = 0D; + double xDifference = 0D; + double yDifference = 0D; + double zDifference = 0D; + for (int index = 0; index < 256; index++) { + double x = index * 0.073D - 4D; + double y = index * 0.011D - 1D; + double z = index * -0.113D + 3D; + double value = first[style].noise(x, y, z); + assertEquals(value, same[style].noise(x, y, z), 0D); + seedDifference += Math.abs(value - highSeed[style].noise(x, y, z)); + xDifference += Math.abs(value - first[style].noise(x + 0.31D, y, z)); + yDifference += Math.abs(value - first[style].noise(x, y + 0.37D, z)); + zDifference += Math.abs(value - first[style].noise(x, y, z + 0.41D)); + } + assertTrue(first[style].getClass().getSimpleName(), seedDifference > 4D); + assertTrue(first[style].getClass().getSimpleName(), xDifference > 4D); + assertTrue(first[style].getClass().getSimpleName(), yDifference > 4D); + assertTrue(first[style].getClass().getSimpleName(), zDifference > 4D); + } + } + + @Test + public void octaveChangesAddDetailAndRestoreTheOriginalSamples() { + for (PatternNoise noise : textures(701L)) { + double[] original = new double[128]; + for (int index = 0; index < original.length; index++) { + original[index] = noise.noise(index * 0.071D, 0.193D, 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.071D, 0.193D, 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.071D, 0.193D, index * -0.113D), 0D); + } + noise.setOctaves(16); + double maximum = noise.noise(0.173D, 0.193D, -0.391D); + noise.setOctaves(Integer.MAX_VALUE); + assertEquals(maximum, noise.noise(0.173D, 0.193D, -0.391D), 0D); + } + } + + @Test + public void textureSamplingHasNoIntegerCellSeams() { + for (PatternNoise noise : textures(913L)) { + for (int boundary = -4; boundary <= 4; boundary++) { + for (int index = 0; index < 64; index++) { + double first = index * 0.073D - 2D; + double second = index * -0.113D + 3D; + assertEquals(noise.noise(boundary - 1.0E-8D, first, second), + noise.noise(boundary + 1.0E-8D, first, second), 1.0E-5D); + assertEquals(noise.noise(first, boundary - 1.0E-8D, second), + noise.noise(first, boundary + 1.0E-8D, second), 1.0E-5D); + assertEquals(noise.noise(first, second, boundary - 1.0E-8D), + noise.noise(first, second, boundary + 1.0E-8D), 1.0E-5D); + } + } + } + } + + private static PatternNoise[] textures(long seed) { + return new PatternNoise[]{new GaborNoise(seed), new MarbleNoise(seed), new ScalesNoise(seed)}; + } +}