From 862baf831faef67e5465e34cc75dfd694a479ea2 Mon Sep 17 00:00:00 2001 From: piexel Date: Sat, 14 Nov 2026 16:46:09 +0100 Subject: [PATCH] Give each fracture axis its own seed - Warp the second and third axes with copies of the fracture reseeded by CNG.fractureAxisSeed, read at the same coordinates as the first axis, instead of reading the fracture at swapped coordinates - Add CNG.reseededForAxis, which caches the copies and drops them when the fracture changes - Shuffle the z axis of CellGenerator with reseeded copies of its noise - Compile the per-axis fracture reads as ordinary terms on their own nodes, so fractured programs stay on the native and GPU kernels - Rewrite the planar scalar chain for one term per axis and level, and drop the signed chain, which no program reaches anymore - Add CNGFractureMirrorTest and update the reference formulas and instrumentation of the fracture tests --- .../art/arcane/volmlib/util/noise/CNG.java | 97 ++++- .../volmlib/util/noise/CellGenerator.java | 12 +- .../noise/program/NoiseProgramCompiler.java | 20 +- .../util/noise/program/ScalarEvaluator.java | 376 +++++------------- .../util/noise/CNGDimensionDispatchTest.java | 8 +- .../util/noise/CNGFractureMirrorTest.java | 81 ++++ .../noise/CNGSignedStateMutationTest.java | 6 +- .../util/noise/CNGUnsignedFractureTest.java | 23 +- 8 files changed, 321 insertions(+), 302 deletions(-) create mode 100644 shared/src/test/java/art/arcane/volmlib/util/noise/CNGFractureMirrorTest.java diff --git a/shared/src/main/java/art/arcane/volmlib/util/noise/CNG.java b/shared/src/main/java/art/arcane/volmlib/util/noise/CNG.java index 380b2f0c..de14e697 100644 --- a/shared/src/main/java/art/arcane/volmlib/util/noise/CNG.java +++ b/shared/src/main/java/art/arcane/volmlib/util/noise/CNG.java @@ -50,6 +50,8 @@ @Data public class CNG { private static final int UNSIGNED_MEMO_CAPACITY = 64; + private static final int FRACTURE_AXES = 3; + private static final long AXIS_SEED_STRIDE = 0x9E3779B97F4A7C15L; private static final ThreadLocal UNSIGNED_MEMO = ThreadLocal.withInitial(UnsignedMemo::new); public static final NoiseInjector ADD = (s, v) -> new double[]{s + v, 1}; public static final NoiseInjector SRC_SUBTRACT = (s, v) -> new double[]{s - v < 0 ? 0 : s - v, -1}; @@ -88,6 +90,16 @@ public class CNG { @Setter(AccessLevel.NONE) @EqualsAndHashCode.Exclude @ToString.Exclude + private NoiseType generatorType; + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + @EqualsAndHashCode.Exclude + @ToString.Exclude + private transient volatile CNG[] axisVariants; + @Getter(AccessLevel.NONE) + @Setter(AccessLevel.NONE) + @EqualsAndHashCode.Exclude + @ToString.Exclude private transient boolean unsignedFractureMemoizable; private transient boolean identityPostFastPath; private transient boolean identitySignedFastPath; @@ -112,6 +124,7 @@ public CNG(RNG random, double opacity, int octaves) { public CNG(RNG random, NoiseType type, double opacity, int octaves) { this(random, type.create(random.nextParallelRNG((long) ((1113334944L * opacity) + 12923)).lmax()), opacity, octaves); + generatorType = type; } public CNG(RNG random, NoiseGenerator generator, double opacity, int octaves) { @@ -140,6 +153,75 @@ public CNG(RNG random, NoiseGenerator generator, double opacity, int octaves) { refreshFastPathState(); } + /** + * Seed of the noise that warps one axis of a fracture. Axis 0 keeps the fracture's own seed; the others get + * uncorrelated seeds, so every axis is warped by its own field read at the same coordinates. + */ + public static long fractureAxisSeed(long fractureSeed, int axis) { + requireFractureAxis(axis); + if (axis == 0) { + return fractureSeed; + } + long mixed = fractureSeed + (axis * AXIS_SEED_STRIDE); + mixed = (mixed ^ (mixed >>> 30)) * 0xBF58476D1CE4E5B9L; + mixed = (mixed ^ (mixed >>> 27)) * 0x94D049BB133111EBL; + return mixed ^ (mixed >>> 31); + } + + private static void requireFractureAxis(int axis) { + if (axis < 0 || axis >= FRACTURE_AXES) { + throw new IllegalArgumentException("Fracture axis must be 0, 1 or 2"); + } + } + + /** + * This noise with its generator reseeded by {@link #fractureAxisSeed(long, int)}, for use as the fracture of the + * given axis. Every other setting is copied, and the nested fracture and children are shared. Axis 0 is this noise. + * The copy is a plain CNG, so a subclass that changes how it samples should override this. + */ + public CNG reseededForAxis(int axis) { + requireFractureAxis(axis); + if (axis == 0) { + return this; + } + CNG[] variants = axisVariants; + if (variants != null && variants[axis] != null) { + return variants[axis]; + } + return publishAxisVariant(variants, axis); + } + + private CNG publishAxisVariant(CNG[] current, int axis) { + CNG variant = createAxisVariant(axis); + CNG[] published = current == null ? new CNG[FRACTURE_AXES] : current.clone(); + published[axis] = variant; + axisVariants = published; + return variant; + } + + private CNG createAxisVariant(int axis) { + long axisSeed = fractureAxisSeed(rng == null ? 0L : rng.getSeed(), axis); + RNG axisRng = new RNG(axisSeed); + CNG variant = generatorType != null + ? new CNG(axisRng, generatorType, opacity, oct) + : new CNG(axisRng, new OffsetNoiseGenerator(generator, axisSeed), opacity, oct); + variant.scale = scale; + variant.bakedScale = bakedScale; + variant.fscale = fscale; + variant.fracture = fracture; + variant.children = children == null ? null : new KList<>(children); + variant.injector = injector; + variant.injectorMode = injectorMode; + variant.noscale = noscale; + variant.power = power; + variant.up = up; + variant.down = down; + variant.patch = patch; + variant.customGenerator = customGenerator; + variant.invalidateLocalState(); + return variant; + } + public static CNG signature(RNG rng) { return signature(rng, NoiseType.SIMPLEX); } @@ -394,6 +476,7 @@ public void setCache(FloatCache cache) { public void setGenerator(NoiseGenerator generator) { this.generator = generator; + generatorType = null; noscale = generator.isNoScale(); if (generator instanceof OctaveNoise octaveNoise) { octaveNoise.setOctaves(oct); @@ -790,7 +873,8 @@ private double getNoise(double x, double z, UnsignedMemo memo) { double fractureX = memo == null ? fracture.noiseFast2D(x, z) : fracture.memoizedNoise2D(x, z, memo); double fx = x + ((fractureX - 0.5D) * fscale); - double fractureZ = memo == null ? fracture.noiseFast2D(z, x) : fracture.memoizedNoise2D(z, x, memo); + CNG fractureForZ = fracture.reseededForAxis(1); + double fractureZ = memo == null ? fractureForZ.noiseFast2D(x, z) : fractureForZ.memoizedNoise2D(x, z, memo); double fz = z + ((fractureZ - 0.5D) * fscale); return generator.noise(fx * scl, fz * scl) * opacity; } @@ -804,8 +888,8 @@ private double getNoise(double x, double y, double z) { } double fx = x + ((fracture.noiseFast3D(x, y, z) - 0.5D) * fscale); - double fy = y + ((fracture.noiseFast2D(y, x) - 0.5D) * fscale); - double fz = z + ((fracture.noiseFast3D(z, x, y) - 0.5D) * fscale); + double fy = y + ((fracture.reseededForAxis(1).noiseFast2D(x, y) - 0.5D) * fscale); + double fz = z + ((fracture.reseededForAxis(2).noiseFast3D(x, y, z) - 0.5D) * fscale); return generator.noise(fx * scl, fy * scl, fz * scl) * opacity; } @@ -1434,6 +1518,7 @@ private void invalidateLocalState() { fastPathStateDirty = true; cache = null; coordCacheIdentity = new Object(); + axisVariants = null; } private boolean referencesChild(CNG candidate) { @@ -1555,7 +1640,7 @@ private double getSignedNoise(double x, double z, SignedCaches caches) { } double fx = x + (localFracture.fractureSigned2D(x, z, caches) * signedFractureScale); - double fz = z + (localFracture.fractureSigned2D(z, x, caches) * signedFractureScale); + double fz = z + (localFracture.reseededForAxis(1).fractureSigned2D(x, z, caches) * signedFractureScale); return signedWithOpacity(localGenerator.noiseSigned(fx * scl, fz * scl)); } @@ -1569,8 +1654,8 @@ private double getSignedNoise(double x, double y, double z, SignedCaches caches) } double fx = x + (localFracture.fractureSigned3D(x, y, z, caches) * signedFractureScale); - double fy = y + (localFracture.fractureSigned2D(y, x, caches) * signedFractureScale); - double fz = z + (localFracture.fractureSigned3D(z, x, y, caches) * signedFractureScale); + double fy = y + (localFracture.reseededForAxis(1).fractureSigned2D(x, y, caches) * signedFractureScale); + double fz = z + (localFracture.reseededForAxis(2).fractureSigned3D(x, y, z, caches) * signedFractureScale); return signedWithOpacity(localGenerator.noiseSigned(fx * scl, fy * scl, fz * scl)); } diff --git a/shared/src/main/java/art/arcane/volmlib/util/noise/CellGenerator.java b/shared/src/main/java/art/arcane/volmlib/util/noise/CellGenerator.java index 90b46aa7..4b2afd31 100644 --- a/shared/src/main/java/art/arcane/volmlib/util/noise/CellGenerator.java +++ b/shared/src/main/java/art/arcane/volmlib/util/noise/CellGenerator.java @@ -26,6 +26,8 @@ public class CellGenerator { private final FastNoiseDouble fn; private final FastNoiseDouble fd; private final CNG cng; + private final CNG secondAxisShuffle; + private final CNG thirdAxisShuffle; @Getter @Setter @@ -39,6 +41,8 @@ public CellGenerator(RNG rng) { shuffle = 128; cellScale = 0.73; cng = CNG.signature(rng.nextParallelRNG(3204)); + secondAxisShuffle = cng.reseededForAxis(1); + thirdAxisShuffle = cng.reseededForAxis(2); RNG rx = rng.nextParallelRNG(8735652); long s = rx.lmax(); fn = new FastNoiseDouble(s); @@ -52,11 +56,11 @@ public CellGenerator(RNG rng) { } public double getDistance(double x, double z) { - return ((fd.GetCellular(((x * cellScale) + (cng.noise(x, z) * shuffle)), ((z * cellScale) + (cng.noise(z, x) * shuffle)))) + 1f) / 2f; + return ((fd.GetCellular(((x * cellScale) + (cng.noise(x, z) * shuffle)), ((z * cellScale) + (secondAxisShuffle.noise(x, z) * shuffle)))) + 1f) / 2f; } public double getDistance(double x, double y, double z) { - return ((fd.GetCellular(((x * cellScale) + (cng.noise(x, y, z) * shuffle)), ((y * cellScale) + (cng.noise(x, y, z) * shuffle)), ((z * cellScale) + (cng.noise(z, y, x) * shuffle)))) + 1f) / 2f; + return ((fd.GetCellular(((x * cellScale) + (cng.noise(x, y, z) * shuffle)), ((y * cellScale) + (cng.noise(x, y, z) * shuffle)), ((z * cellScale) + (thirdAxisShuffle.noise(x, y, z) * shuffle)))) + 1f) / 2f; } public double getValue(double x, double z, int possibilities) { @@ -64,7 +68,7 @@ public double getValue(double x, double z, int possibilities) { return 0; } - return ((fn.GetCellular(((x * cellScale) + (cng.noise(x, z) * shuffle)), ((z * cellScale) + (cng.noise(z, x) * shuffle))) + 1f) / 2f) * (possibilities - 1); + return ((fn.GetCellular(((x * cellScale) + (cng.noise(x, z) * shuffle)), ((z * cellScale) + (secondAxisShuffle.noise(x, z) * shuffle))) + 1f) / 2f) * (possibilities - 1); } public double getValue(double x, double y, double z, int possibilities) { @@ -72,7 +76,7 @@ public double getValue(double x, double y, double z, int possibilities) { return 0; } - return ((fn.GetCellular(((x * cellScale) + (cng.noise(x, z) * shuffle)), ((y * 8 * cellScale) + (cng.noise(x, y * 8) * shuffle)), ((z * cellScale) + (cng.noise(z, x) * shuffle))) + 1f) / 2f) * (possibilities - 1); + return ((fn.GetCellular(((x * cellScale) + (cng.noise(x, z) * shuffle)), ((y * 8 * cellScale) + (cng.noise(x, y * 8) * shuffle)), ((z * cellScale) + (secondAxisShuffle.noise(x, z) * shuffle))) + 1f) / 2f) * (possibilities - 1); } public int getIndex(double x, double z, int possibilities) { diff --git a/shared/src/main/java/art/arcane/volmlib/util/noise/program/NoiseProgramCompiler.java b/shared/src/main/java/art/arcane/volmlib/util/noise/program/NoiseProgramCompiler.java index 234a4ef9..5c5857a3 100644 --- a/shared/src/main/java/art/arcane/volmlib/util/noise/program/NoiseProgramCompiler.java +++ b/shared/src/main/java/art/arcane/volmlib/util/noise/program/NoiseProgramCompiler.java @@ -128,7 +128,8 @@ private int node(CNG cng) { entry.identityPost = cng.isIdentityPostFastPath(); entry.identitySigned = cng.isIdentitySignedFastPath(); if (cng.getFracture() != null && !cng.isNoscale()) { - entry.fracture = node(cng.getFracture()); + entry.fractureSource = cng.getFracture(); + entry.fracture = node(entry.fractureSource); } KList children = cng.getChildren(); if (children != null && !children.isEmpty()) { @@ -191,7 +192,7 @@ private void resolveDependencies(NodeEntry entry, TermEntry term) { case NoiseProgram.UNSIGNED_2D, NoiseProgram.NOISE_2D -> { if (fracture >= 0) { term.dep0 = term(fracture, NoiseProgram.UNSIGNED_2D, a0, a1, 0); - term.dep1 = term(fracture, NoiseProgram.UNSIGNED_2D, a1, a0, 0); + term.dep1 = term(fractureNode(entry, 1), NoiseProgram.UNSIGNED_2D, a0, a1, 0); } term.applyPost = term.mode == NoiseProgram.NOISE_2D || !entry.identityPost; if (term.applyPost && entry.children != null) { @@ -203,14 +204,14 @@ private void resolveDependencies(NodeEntry entry, TermEntry term) { term.dep0 = term(term.node, NoiseProgram.UNSIGNED_2D, a0, a1, 0); } else if (fracture >= 0) { term.dep0 = term(fracture, NoiseProgram.SIGNED_2D, a0, a1, 0); - term.dep1 = term(fracture, NoiseProgram.SIGNED_2D, a1, a0, 0); + term.dep1 = term(fractureNode(entry, 1), NoiseProgram.SIGNED_2D, a0, a1, 0); } } case NoiseProgram.UNSIGNED_3D, NoiseProgram.NOISE_3D -> { if (fracture >= 0) { term.dep0 = term(fracture, NoiseProgram.UNSIGNED_3D, a0, a1, a2); - term.dep1 = term(fracture, NoiseProgram.UNSIGNED_2D, a1, a0, 0); - term.dep2 = term(fracture, NoiseProgram.UNSIGNED_3D, a2, a0, a1); + term.dep1 = term(fractureNode(entry, 1), NoiseProgram.UNSIGNED_2D, a0, a1, 0); + term.dep2 = term(fractureNode(entry, 2), NoiseProgram.UNSIGNED_3D, a0, a1, a2); } term.applyPost = term.mode == NoiseProgram.NOISE_3D || !entry.identityPost; if (term.applyPost && entry.children != null) { @@ -222,13 +223,17 @@ private void resolveDependencies(NodeEntry entry, TermEntry term) { term.dep0 = term(term.node, NoiseProgram.UNSIGNED_3D, a0, a1, a2); } else if (fracture >= 0) { term.dep0 = term(fracture, NoiseProgram.SIGNED_3D, a0, a1, a2); - term.dep1 = term(fracture, NoiseProgram.SIGNED_2D, a1, a0, 0); - term.dep2 = term(fracture, NoiseProgram.SIGNED_3D, a2, a0, a1); + term.dep1 = term(fractureNode(entry, 1), NoiseProgram.SIGNED_2D, a0, a1, 0); + term.dep2 = term(fractureNode(entry, 2), NoiseProgram.SIGNED_3D, a0, a1, a2); } } } } + private int fractureNode(NodeEntry entry, int axis) { + return node(entry.fractureSource.reseededForAxis(axis)); + } + private int[] childTerms(NodeEntry entry, int mode, int axis0, int axis1, int axis2) { int[] childTerms = new int[entry.children.length]; for (int child = 0; child < childTerms.length; child++) { @@ -253,6 +258,7 @@ static final class NodeEntry { boolean identityPost; boolean identitySigned; ChildInjector injector; + CNG fractureSource; int fracture = -1; int[] children; diff --git a/shared/src/main/java/art/arcane/volmlib/util/noise/program/ScalarEvaluator.java b/shared/src/main/java/art/arcane/volmlib/util/noise/program/ScalarEvaluator.java index d0c370ba..cb5837eb 100644 --- a/shared/src/main/java/art/arcane/volmlib/util/noise/program/ScalarEvaluator.java +++ b/shared/src/main/java/art/arcane/volmlib/util/noise/program/ScalarEvaluator.java @@ -4,44 +4,12 @@ abstract class ScalarEvaluator { private static final ThreadLocal SCRATCH = ThreadLocal.withInitial(NoiseScratch::new); static ScalarEvaluator of(NoiseProgram program) { - int[] chain = chain(program); - if (chain != null && program.volumetric && SignedChain.accepts(program, chain)) { - return new SignedChain(program, chain); - } - if (chain != null && !program.volumetric && PlanarChain.accepts(program, chain)) { - return new PlanarChain(program, chain); - } - return new Interpreted(program); + PlanarChain chain = program.volumetric ? null : PlanarChain.tryCreate(program); + return chain != null ? chain : new Interpreted(program); } abstract double sample(double x, double y, double z); - private static int[] chain(NoiseProgram program) { - int root = program.termNode[program.rootTerm]; - int length = 0; - for (int node = root; node >= 0; node = program.nodeFracture[node]) { - if (program.nodeOpaque[node] || program.nodeChildren[node] != null || length > program.nodeCount) { - return null; - } - length++; - } - int[] chain = new int[length]; - int level = 0; - for (int node = root; node >= 0; node = program.nodeFracture[node]) { - chain[level++] = node; - } - return chain; - } - - private static int level(int[] chain, int node) { - for (int level = 0; level < chain.length; level++) { - if (chain[level] == node) { - return level; - } - } - return -1; - } - private static final class Interpreted extends ScalarEvaluator { private final NoiseProgram program; @@ -68,261 +36,125 @@ private Interpreted(NoiseProgram program) { } } + /** + * A planar program that is a plain chain of fractures: below the root, every level is one term per axis, and + * both terms are warped by the same pair of terms on the level below. + */ private static final class PlanarChain extends ScalarEvaluator { - private final int leaf; - private final PrimitiveKernel[] kernels; - private final double[] scale; - private final double[] fractureScale; - private final double[] opacity; - private final boolean[] post; - private final double[] power; - private final double[] down; - private final double[] up; - private final double[] patch; + private static final int TERMS_PER_LEVEL = 2; - private PlanarChain(NoiseProgram program, int[] chain) { - int levels = chain.length; - leaf = levels - 1; - kernels = new PrimitiveKernel[levels]; - scale = new double[levels]; - fractureScale = new double[levels]; - opacity = new double[levels]; - post = new boolean[levels]; - power = new double[levels]; - down = new double[levels]; - up = new double[levels]; - patch = new double[levels]; - for (int level = 0; level < levels; level++) { - int node = chain[level]; - kernels[level] = program.nodeKernels[node]; - scale[level] = program.nodeScale[node]; - fractureScale[level] = program.nodeFractureScale[node]; - opacity[level] = program.nodeOpacity[node]; - power[level] = program.nodePower[node]; - down[level] = program.nodeDown[node]; - up[level] = program.nodeUp[node]; - patch[level] = program.nodePatch[node]; - } - for (int term = 0; term < program.termCount; term++) { - post[level(chain, program.termNode[term])] = program.termApplyPost[term]; - } - } + private final PlanarStep root; + private final PlanarStep[] firstAxis; + private final PlanarStep[] secondAxis; - private static boolean accepts(NoiseProgram program, int[] chain) { - for (int term = 0; term < program.termCount; term++) { - if (program.termMode[term] != NoiseProgram.UNSIGNED_2D || level(chain, program.termNode[term]) < 0) { - return false; - } - } - return true; - } - - @Override - double sample(double x, double z, double ignored) { - int last = leaf; - PrimitiveKernel leafKernel = kernels[last]; - double leafScale = scale[last]; - double forward = finish(last, leafKernel.noise2(x * leafScale, z * leafScale) * opacity[last]); - if (last == 0) { - return forward; - } - double reverse = finish(last, leafKernel.noise2(z * leafScale, x * leafScale) * opacity[last]); - for (int level = last - 1; level > 0; level--) { - PrimitiveKernel kernel = kernels[level]; - double levelScale = scale[level]; - double displacement = fractureScale[level]; - double nextForward = finish(level, kernel.noise2((x + ((forward - 0.5D) * displacement)) * levelScale, - (z + ((reverse - 0.5D) * displacement)) * levelScale) * opacity[level]); - double nextReverse = finish(level, kernel.noise2((z + ((reverse - 0.5D) * displacement)) * levelScale, - (x + ((forward - 0.5D) * displacement)) * levelScale) * opacity[level]); - forward = nextForward; - reverse = nextReverse; - } - double rootScale = scale[0]; - double displacement = fractureScale[0]; - return finish(0, kernels[0].noise2((x + ((forward - 0.5D) * displacement)) * rootScale, - (z + ((reverse - 0.5D) * displacement)) * rootScale) * opacity[0]); + private PlanarChain(PlanarStep root, PlanarStep[] firstAxis, PlanarStep[] secondAxis) { + this.root = root; + this.firstAxis = firstAxis; + this.secondAxis = secondAxis; } - private double finish(int level, double raw) { - if (!post[level]) { - return raw; - } - double exponent = power[level]; - double n = exponent != 1D ? (raw < 0 ? -Math.pow(Math.abs(raw), exponent) : Math.pow(raw, exponent)) : raw; - return ((n / 1D) - down[level] + up[level]) * patch[level]; - } - } - - private static final class SignedChain extends ScalarEvaluator { - private static final int P0 = 1; - private static final int P1 = 1 << 1; - private static final int P2 = 1 << 2; - private static final int XY = 1 << 3; - private static final int YX = 1 << 4; - private static final int XZ = 1 << 5; - private static final int ZX = 1 << 6; - private static final int YZ = 1 << 7; - private static final int ZY = 1 << 8; - - private final int leaf; - private final PrimitiveKernel[] kernels; - private final double[] scale; - private final double[] fractureScale; - private final double[] opacity; - private final boolean[] unity; - private final int[] masks; - - private SignedChain(NoiseProgram program, int[] chain) { - int levels = chain.length; - leaf = levels - 1; - kernels = new PrimitiveKernel[levels]; - scale = new double[levels]; - fractureScale = new double[levels]; - opacity = new double[levels]; - unity = new boolean[levels]; - masks = new int[levels]; - for (int level = 0; level < levels; level++) { - int node = chain[level]; - kernels[level] = program.nodeKernels[node]; - scale[level] = program.nodeScale[node]; - fractureScale[level] = program.nodeSignedFractureScale[node]; - opacity[level] = program.nodeOpacity[node]; - unity[level] = program.nodeUnityOpacity[node]; - } - for (int term = 0; term < program.termCount; term++) { - masks[level(chain, program.termNode[term])] |= bit(program, term); - } - } - - private static boolean accepts(NoiseProgram program, int[] chain) { - for (int node : chain) { - if (!program.nodeIdentitySigned[node]) { - return false; - } + private static PlanarChain tryCreate(NoiseProgram program) { + int depth = fractureDepth(program); + if (depth < 0 || program.termCount != 1 + (TERMS_PER_LEVEL * depth)) { + return null; } - for (int term = 0; term < program.termCount; term++) { - int mode = program.termMode[term]; - if ((mode != NoiseProgram.SIGNED_3D && mode != NoiseProgram.SIGNED_2D) || level(chain, program.termNode[term]) < 0) { - return false; + PlanarStep[] firstAxis = new PlanarStep[depth]; + PlanarStep[] secondAxis = new PlanarStep[depth]; + int term = program.rootTerm; + for (int level = 0; level < depth; level++) { + firstAxis[level] = new PlanarStep(program, program.termDep0[term]); + secondAxis[level] = new PlanarStep(program, program.termDep1[term]); + term = program.termDep0[term]; + } + return new PlanarChain(new PlanarStep(program, program.rootTerm), firstAxis, secondAxis); + } + + private static int fractureDepth(NoiseProgram program) { + int term = program.rootTerm; + if (!isPlainPlanarTerm(program, term)) { + return -1; + } + int depth = 0; + while (program.termDep0[term] >= 0) { + int first = program.termDep0[term]; + int second = program.termDep1[term]; + if (!isPlainPlanarTerm(program, first) || !isPlainPlanarTerm(program, second) + || program.termDep0[first] != program.termDep0[second] + || program.termDep1[first] != program.termDep1[second]) { + return -1; } + depth++; + term = first; } - return true; + return depth; } - private static int bit(NoiseProgram program, int term) { - int a0 = program.termAxis0[term]; - int a1 = program.termAxis1[term]; - if (program.termMode[term] == NoiseProgram.SIGNED_3D) { - return a0 == 0 ? P0 : a0 == 2 ? P1 : P2; - } - if (a0 == 0) { - return a1 == 1 ? XY : XZ; - } - if (a0 == 1) { - return a1 == 0 ? YX : YZ; - } - return a1 == 0 ? ZX : ZY; + private static boolean isPlainPlanarTerm(NoiseProgram program, int term) { + int node = program.termNode[term]; + return program.termMode[term] == NoiseProgram.UNSIGNED_2D + && program.termAxis0[term] == 0 + && program.termAxis1[term] == 1 + && !program.nodeOpaque[node] + && program.nodeChildren[node] == null; } @Override - double sample(double x, double y, double z) { - int last = leaf; - PrimitiveKernel kernel = kernels[last]; - double s = scale[last]; - int mask = masks[last]; - double p0 = 0D; - double p1 = 0D; - double p2 = 0D; - double xy = 0D; - double yx = 0D; - double xz = 0D; - double zx = 0D; - double yz = 0D; - double zy = 0D; - if ((mask & P0) != 0) { - p0 = signed(last, kernel.signed3(x * s, y * s, z * s)); - } - if ((mask & P1) != 0) { - p1 = signed(last, kernel.signed3(z * s, x * s, y * s)); - } - if ((mask & P2) != 0) { - p2 = signed(last, kernel.signed3(y * s, z * s, x * s)); - } - if ((mask & XY) != 0) { - xy = signed(last, kernel.signed2(x * s, y * s)); - } - if ((mask & YX) != 0) { - yx = signed(last, kernel.signed2(y * s, x * s)); - } - if ((mask & XZ) != 0) { - xz = signed(last, kernel.signed2(x * s, z * s)); - } - if ((mask & ZX) != 0) { - zx = signed(last, kernel.signed2(z * s, x * s)); - } - if ((mask & YZ) != 0) { - yz = signed(last, kernel.signed2(y * s, z * s)); + double sample(double x, double z, double ignored) { + int leaf = firstAxis.length - 1; + if (leaf < 0) { + return root.sample(x, z); } - if ((mask & ZY) != 0) { - zy = signed(last, kernel.signed2(z * s, y * s)); + double first = firstAxis[leaf].sample(x, z); + double second = secondAxis[leaf].sample(x, z); + for (int level = leaf - 1; level >= 0; level--) { + double nextFirst = firstAxis[level].sampleWarped(x, z, first, second); + double nextSecond = secondAxis[level].sampleWarped(x, z, first, second); + first = nextFirst; + second = nextSecond; } - for (int level = last - 1; level >= 0; level--) { - kernel = kernels[level]; - s = scale[level]; - double f = fractureScale[level]; - mask = masks[level]; - double n0 = p0; - double n1 = p1; - double n2 = p2; - double nxy = xy; - double nyx = yx; - double nxz = xz; - double nzx = zx; - double nyz = yz; - double nzy = zy; - if ((mask & P0) != 0) { - n0 = signed(level, kernel.signed3((x + (p0 * f)) * s, (y + (yx * f)) * s, (z + (p1 * f)) * s)); - } - if ((mask & P1) != 0) { - n1 = signed(level, kernel.signed3((z + (p1 * f)) * s, (x + (xz * f)) * s, (y + (p2 * f)) * s)); - } - if ((mask & P2) != 0) { - n2 = signed(level, kernel.signed3((y + (p2 * f)) * s, (z + (zy * f)) * s, (x + (p0 * f)) * s)); - } - if ((mask & XY) != 0) { - nxy = signed(level, kernel.signed2((x + (xy * f)) * s, (y + (yx * f)) * s)); - } - if ((mask & YX) != 0) { - nyx = signed(level, kernel.signed2((y + (yx * f)) * s, (x + (xy * f)) * s)); - } - if ((mask & XZ) != 0) { - nxz = signed(level, kernel.signed2((x + (xz * f)) * s, (z + (zx * f)) * s)); - } - if ((mask & ZX) != 0) { - nzx = signed(level, kernel.signed2((z + (zx * f)) * s, (x + (xz * f)) * s)); - } - if ((mask & YZ) != 0) { - nyz = signed(level, kernel.signed2((y + (yz * f)) * s, (z + (zy * f)) * s)); - } - if ((mask & ZY) != 0) { - nzy = signed(level, kernel.signed2((z + (zy * f)) * s, (y + (yz * f)) * s)); - } - p0 = n0; - p1 = n1; - p2 = n2; - xy = nxy; - yx = nyx; - xz = nxz; - zx = nzx; - yz = nyz; - zy = nzy; - } - return p0; + return root.sampleWarped(x, z, first, second); } + } - private double signed(int level, double value) { - return unity[level] ? value : ((value + 1D) * opacity[level]) - 1D; + private static final class PlanarStep { + private final PrimitiveKernel kernel; + private final double scale; + private final double fractureScale; + private final double opacity; + private final boolean post; + private final double power; + private final double down; + private final double up; + private final double patch; + + private PlanarStep(NoiseProgram program, int term) { + int node = program.termNode[term]; + kernel = program.nodeKernels[node]; + scale = program.nodeScale[node]; + fractureScale = program.nodeFractureScale[node]; + opacity = program.nodeOpacity[node]; + post = program.termApplyPost[term]; + power = program.nodePower[node]; + down = program.nodeDown[node]; + up = program.nodeUp[node]; + patch = program.nodePatch[node]; + } + + private double sample(double x, double z) { + return finish(kernel.noise2(x * scale, z * scale) * opacity); + } + + private double sampleWarped(double x, double z, double firstWarp, double secondWarp) { + return finish(kernel.noise2((x + ((firstWarp - 0.5D) * fractureScale)) * scale, + (z + ((secondWarp - 0.5D) * fractureScale)) * scale) * opacity); + } + + private double finish(double raw) { + if (!post) { + return raw; + } + double n = power != 1D ? (raw < 0 ? -Math.pow(Math.abs(raw), power) : Math.pow(raw, power)) : raw; + return ((n / 1D) - down + up) * patch; } } } diff --git a/shared/src/test/java/art/arcane/volmlib/util/noise/CNGDimensionDispatchTest.java b/shared/src/test/java/art/arcane/volmlib/util/noise/CNGDimensionDispatchTest.java index 9008fca2..04560e58 100644 --- a/shared/src/test/java/art/arcane/volmlib/util/noise/CNGDimensionDispatchTest.java +++ b/shared/src/test/java/art/arcane/volmlib/util/noise/CNGDimensionDispatchTest.java @@ -69,12 +69,14 @@ public void fractureSamplesAndDisplacesTheRequestedDimensions() { double x = -13.25D; double y = 5.5D; double z = 37.75D; + NoiseGenerator secondAxisSource = fractureSource.offset(CNG.fractureAxisSeed(2L, 1)); + NoiseGenerator thirdAxisSource = fractureSource.offset(CNG.fractureAxisSeed(2L, 2)); double displaced1D = x + (fractureSource.noise(x) - 0.5D) * 12D; double displacedX2D = x + (fractureSource.noise(x, z) - 0.5D) * 12D; - double displacedZ2D = z + (fractureSource.noise(z, x) - 0.5D) * 12D; + double displacedZ2D = z + (secondAxisSource.noise(x, z) - 0.5D) * 12D; double displacedX3D = x + (fractureSource.noise(x, y, z) - 0.5D) * 12D; - double displacedY3D = y + (fractureSource.noise(y, x) - 0.5D) * 12D; - double displacedZ3D = z + (fractureSource.noise(z, x, y) - 0.5D) * 12D; + double displacedY3D = y + (secondAxisSource.noise(x, y) - 0.5D) * 12D; + double displacedZ3D = z + (thirdAxisSource.noise(x, y, z) - 0.5D) * 12D; double expected1D = source.noise(displaced1D * 0.35D) * 0.7D; double expected2D = source.noise(displacedX2D * 0.35D, displacedZ2D * 0.35D) * 0.7D; double expected3D = source.noise(displacedX3D * 0.35D, displacedY3D * 0.35D, displacedZ3D * 0.35D) * 0.7D; diff --git a/shared/src/test/java/art/arcane/volmlib/util/noise/CNGFractureMirrorTest.java b/shared/src/test/java/art/arcane/volmlib/util/noise/CNGFractureMirrorTest.java new file mode 100644 index 00000000..a495dbc7 --- /dev/null +++ b/shared/src/test/java/art/arcane/volmlib/util/noise/CNGFractureMirrorTest.java @@ -0,0 +1,81 @@ +package art.arcane.volmlib.util.noise; + +import art.arcane.volmlib.util.math.RNG; +import org.junit.Test; + +import static org.junit.Assert.assertTrue; + +public class CNGFractureMirrorTest { + private static final int GRID_SIZE = 40; + private static final double GRID_STEP = 7.3D; + private static final double HEIGHT = 11.7D; + private static final double MIRRORED = 1.0E-12D; + private static final int ALLOWED_MIRRORED_SAMPLES = 2; + + @Test + public void fracturedNoiseIsNotMirroredAcrossTheDiagonal() { + assertNotMirrored("2D", (noise, a, b) -> noise.noise(a, b)); + assertNotMirrored("2D fast", (noise, a, b) -> noise.noiseFast2D(a, b)); + assertNotMirrored("2D signed", (noise, a, b) -> noise.noiseFastSigned2D(a, b)); + assertNotMirrored("generic", (noise, a, b) -> noise.noise(new double[]{a, b})); + assertNotMirrored("3D", (noise, a, b) -> noise.noise(a, HEIGHT, b)); + assertNotMirrored("3D fast", (noise, a, b) -> noise.noiseFast3D(a, HEIGHT, b)); + assertNotMirrored("3D signed", (noise, a, b) -> noise.noiseFastSigned3D(a, HEIGHT, b)); + assertNotMirrored("generic 3D", (noise, a, b) -> noise.noise(new double[]{a, HEIGHT, b})); + } + + @Test + public void theDiagonalSymmetricGeneratorIsMirroredWithoutAFracture() { + CNG unfractured = new CNG(new RNG(17L), new DiagonalSymmetricGenerator(), 1D, 1); + + assertTrue(mirroredSamples(unfractured, (noise, a, b) -> noise.noise(a, b)) > ALLOWED_MIRRORED_SAMPLES); + assertTrue(mirroredSamples(unfractured, (noise, a, b) -> noise.noise(a, HEIGHT, b)) > ALLOWED_MIRRORED_SAMPLES); + } + + private void assertNotMirrored(String path, Sampler sampler) { + int mirroredSamples = mirroredSamples(fracturedSymmetricNoise(), sampler); + assertTrue(path + " fracture mirrors the noise across x = z at " + mirroredSamples + " samples", + mirroredSamples <= ALLOWED_MIRRORED_SAMPLES); + } + + private static int mirroredSamples(CNG noise, Sampler sampler) { + int mirroredSamples = 0; + for (int i = 0; i < GRID_SIZE; i++) { + for (int j = i + 1; j < GRID_SIZE; j++) { + double a = i * GRID_STEP; + double b = j * GRID_STEP + 0.5D; + if (Math.abs(sampler.sample(noise, a, b) - sampler.sample(noise, b, a)) < MIRRORED) { + mirroredSamples++; + } + } + } + return mirroredSamples; + } + + private static CNG fracturedSymmetricNoise() { + CNG fracture = new CNG(new RNG(91L), NoiseType.SIMPLEX, 1D, 1).scale(0.03D); + return new CNG(new RNG(17L), new DiagonalSymmetricGenerator(), 1D, 1).fractureWith(fracture, 40D); + } + + @FunctionalInterface + private interface Sampler { + double sample(CNG noise, double a, double b); + } + + private static final class DiagonalSymmetricGenerator implements NoiseGenerator { + @Override + public double noise(double x) { + return noise(x, 0D, 0D); + } + + @Override + public double noise(double x, double z) { + return noise(x, z, 0D); + } + + @Override + public double noise(double x, double y, double z) { + return 0.5D + (Math.sin(x * 0.11D) + Math.sin(y * 0.11D) + Math.sin(z * 0.11D)) / 6D; + } + } +} diff --git a/shared/src/test/java/art/arcane/volmlib/util/noise/CNGSignedStateMutationTest.java b/shared/src/test/java/art/arcane/volmlib/util/noise/CNGSignedStateMutationTest.java index 7130b592..b981759b 100644 --- a/shared/src/test/java/art/arcane/volmlib/util/noise/CNGSignedStateMutationTest.java +++ b/shared/src/test/java/art/arcane/volmlib/util/noise/CNGSignedStateMutationTest.java @@ -65,10 +65,12 @@ public void nestedFractureOverridesRetainParentAndChildMutationBoundaries() { fracture.scale(0.25D); }; parent.fractureWith(fracture, 8D); + NoiseGenerator secondAxis = fractureSource.offset(CNG.fractureAxisSeed(13L, 1)); + NoiseGenerator thirdAxis = fractureSource.offset(CNG.fractureAxisSeed(13L, 2)); double shiftedX = 4D + value(threeDimensions, 1D, 1.5D, 2D) * 4D; - double shiftedY = 6D + value(false, 1.5D, 0D, 1D) * 4D; + double shiftedY = 6D + secondAxis.noiseSigned(1D, 1.5D) * 4D; double shiftedZ = 8D + (threeDimensions - ? value(true, 2D, 1D, 1.5D) : value(false, 2D, 0D, 1D)) * 4D; + ? thirdAxis.noiseSigned(1D, 1.5D, 2D) : secondAxis.noiseSigned(1D, 2D)) * 4D; assertEquals(value(threeDimensions, shiftedX, shiftedY, shiftedZ), sample(parent, threeDimensions), 0D); assertEquals(value(threeDimensions, shiftedX * 0.5D, shiftedY * 0.5D, shiftedZ * 0.5D), sample(parent, threeDimensions), 0D); diff --git a/shared/src/test/java/art/arcane/volmlib/util/noise/CNGUnsignedFractureTest.java b/shared/src/test/java/art/arcane/volmlib/util/noise/CNGUnsignedFractureTest.java index 91239986..da1b1068 100644 --- a/shared/src/test/java/art/arcane/volmlib/util/noise/CNGUnsignedFractureTest.java +++ b/shared/src/test/java/art/arcane/volmlib/util/noise/CNGUnsignedFractureTest.java @@ -160,7 +160,9 @@ public double noiseFast2D(double x, double z) { }); assertFalse(eligible(noise)); noise.noiseFast2D(12D, -8D); - assertEquals(32, calls.get()); + int leafReads = 32; + int firstAxisLeafReads = leafReads / 2; + assertEquals(firstAxisLeafReads, calls.get()); } @Test @@ -382,14 +384,19 @@ private static Field declaredField(Class type, String name) throws Reflective private static void instrument(CNG noise, Runnable onSample) throws ReflectiveOperationException { for (CNG node = noise; node != null; node = node.getFracture()) { - NoiseGenerator base = node.getGenerator(); - while (base instanceof OffsetNoiseGenerator offset) { - base = offset.getBase(); - } - Field field = declaredField(SimplexNoise.class, "n"); - FastNoiseDouble original = (FastNoiseDouble) field.get(base); - field.set(base, new ObservedSimplex(original, onSample)); + instrumentGenerator(node, onSample); + instrumentGenerator(node.reseededForAxis(1), onSample); + } + } + + private static void instrumentGenerator(CNG node, Runnable onSample) throws ReflectiveOperationException { + NoiseGenerator base = node.getGenerator(); + while (base instanceof OffsetNoiseGenerator offset) { + base = offset.getBase(); } + Field field = declaredField(SimplexNoise.class, "n"); + FastNoiseDouble original = (FastNoiseDouble) field.get(base); + field.set(base, new ObservedSimplex(original, onSample)); } private static final class ObservedSimplex extends FastNoiseDouble {