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

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