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 380b2f0..de14e69 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 90b46aa..4b2afd3 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 234a4ef..5c5857a 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 d0c370b..cb5837e 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 9008fca..04560e5 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 0000000..a495dbc --- /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 7130b59..b981759 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 9123998..da1b106 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 {