diff --git a/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/NoiseAddon.java b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/NoiseAddon.java index 2be9510552..bb4b09c779 100644 --- a/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/NoiseAddon.java +++ b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/NoiseAddon.java @@ -68,6 +68,8 @@ import com.dfsek.terra.api.event.events.config.pack.ConfigPackPreLoadEvent; import com.dfsek.terra.api.event.functional.FunctionalEventHandler; import com.dfsek.terra.api.inject.annotations.Inject; +import com.dfsek.terra.api.noise.CellularDistanceFunction; +import com.dfsek.terra.api.noise.CellularReturnType; import com.dfsek.terra.api.noise.DerivativeNoiseSampler; import com.dfsek.terra.api.noise.NoiseSampler; import com.dfsek.terra.api.registry.CheckedRegistry; @@ -94,10 +96,10 @@ public void initialize() { CheckedRegistry>> noiseRegistry = event.getPack().getOrCreateRegistry( NOISE_SAMPLER_TOKEN); event.getPack() - .applyLoader(CellularSampler.DistanceFunction.class, - (type, o, loader, depthTracker) -> CellularSampler.DistanceFunction.valueOf((String) o)) - .applyLoader(CellularSampler.ReturnType.class, - (type, o, loader, depthTracker) -> CellularSampler.ReturnType.valueOf((String) o)) + .applyLoader(CellularDistanceFunction.class, + (type, o, loader, depthTracker) -> CellularDistanceFunction.valueOf((String) o)) + .applyLoader(CellularReturnType.class, + (type, o, loader, depthTracker) -> CellularReturnType.valueOf((String) o)) .applyLoader(DistanceSampler.DistanceFunction.class, (type, o, loader, depthTracker) -> DistanceSampler.DistanceFunction.valueOf((String) o)) .applyLoader(DimensionApplicableNoiseSampler.class, DimensionApplicableNoiseSampler::new) diff --git a/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/config/templates/noise/CellularNoiseTemplate.java b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/config/templates/noise/CellularNoiseTemplate.java index 4716318a80..61d5e8d60e 100644 --- a/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/config/templates/noise/CellularNoiseTemplate.java +++ b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/config/templates/noise/CellularNoiseTemplate.java @@ -13,6 +13,8 @@ import com.dfsek.terra.addons.noise.samplers.noise.CellularSampler; import com.dfsek.terra.addons.noise.samplers.noise.simplex.OpenSimplex2Sampler; import com.dfsek.terra.api.config.meta.Meta; +import com.dfsek.terra.api.noise.CellularDistanceFunction; +import com.dfsek.terra.api.noise.CellularReturnType; import com.dfsek.terra.api.noise.NoiseSampler; @@ -20,11 +22,11 @@ public class CellularNoiseTemplate extends NoiseTemplate { @Value("distance") @Default - private CellularSampler.@Meta DistanceFunction cellularDistanceFunction = CellularSampler.DistanceFunction.EuclideanSq; + private @Meta CellularDistanceFunction cellularDistanceFunction = CellularDistanceFunction.EuclideanSq; @Value("return") @Default - private CellularSampler.@Meta ReturnType cellularReturnType = CellularSampler.ReturnType.Distance; + private @Meta CellularReturnType cellularReturnType = CellularReturnType.Distance; @Value("jitter") @Default diff --git a/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/samplers/noise/CellularSampler.java b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/samplers/noise/CellularSampler.java index fea5ccec56..46e4e72cd9 100644 --- a/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/samplers/noise/CellularSampler.java +++ b/common/addons/config-noise-function/src/main/java/com/dfsek/terra/addons/noise/samplers/noise/CellularSampler.java @@ -8,6 +8,8 @@ package com.dfsek.terra.addons.noise.samplers.noise; import com.dfsek.terra.addons.noise.samplers.noise.simplex.OpenSimplex2Sampler; +import com.dfsek.terra.api.noise.CellularDistanceFunction; +import com.dfsek.terra.api.noise.CellularReturnType; import com.dfsek.terra.api.noise.NoiseSampler; @@ -192,8 +194,8 @@ public class CellularSampler extends NoiseFunction { }; - private DistanceFunction distanceFunction = DistanceFunction.EuclideanSq; - private ReturnType returnType = ReturnType.Distance; + private CellularDistanceFunction distanceFunction = CellularDistanceFunction.EuclideanSq; + private CellularReturnType returnType = CellularReturnType.Distance; private double jitterModifier = 1.0; private NoiseSampler noiseLookup; @@ -204,7 +206,7 @@ public CellularSampler() { noiseLookup = new OpenSimplex2Sampler(); } - public void setDistanceFunction(DistanceFunction distanceFunction) { + public void setDistanceFunction(CellularDistanceFunction distanceFunction) { this.distanceFunction = distanceFunction; } @@ -216,7 +218,7 @@ public void setNoiseLookup(NoiseSampler noiseLookup) { this.noiseLookup = noiseLookup; } - public void setReturnType(ReturnType returnType) { + public void setReturnType(CellularReturnType returnType) { this.returnType = returnType; } @@ -277,10 +279,10 @@ public double getNoiseRaw(long sl, double x, double y) { xPrimed += PRIME_X; } - if(distanceFunction == DistanceFunction.Euclidean && returnType != ReturnType.CellValue) { + if(distanceFunction == CellularDistanceFunction.Euclidean && returnType != CellularReturnType.CellValue) { distance0 = Math.sqrt(distance0); - if (returnType != ReturnType.Distance) { + if (returnType != CellularReturnType.Distance) { distance1 = Math.sqrt(distance1); } } @@ -369,10 +371,10 @@ public double getNoiseRaw(long sl, double x, double y, double z) { xPrimed += PRIME_X; } - if(distanceFunction == DistanceFunction.Euclidean && returnType != ReturnType.CellValue) { + if(distanceFunction == CellularDistanceFunction.Euclidean && returnType != CellularReturnType.CellValue) { distance0 = Math.sqrt(distance0); - if (returnType != ReturnType.Distance) { + if (returnType != CellularReturnType.Distance) { distance1 = Math.sqrt(distance1); } } @@ -395,30 +397,4 @@ public double getNoiseRaw(long sl, double x, double y, double z) { case Angle -> Math.atan2(y / frequency - centerY, x / frequency - centerX); }; } - - public enum DistanceFunction { - Euclidean, - EuclideanSq, - Manhattan, - Hybrid - } - - - public enum ReturnType { - CellValue, - Distance, - Distance2, - Distance2Add, - Distance2Sub, - Distance2Mul, - Distance2Div, - NoiseLookup, - LocalNoiseLookup, - Distance3, - Distance3Add, - Distance3Sub, - Distance3Mul, - Distance3Div, - Angle - } } diff --git a/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/ImageLibraryAddon.java b/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/ImageLibraryAddon.java index b383c4ca75..cc77c89050 100644 --- a/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/ImageLibraryAddon.java +++ b/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/ImageLibraryAddon.java @@ -15,6 +15,7 @@ import com.dfsek.terra.addons.image.config.colorsampler.mutate.TranslateColorSamplerTemplate; import com.dfsek.terra.addons.image.config.image.ImageTemplate; import com.dfsek.terra.addons.image.config.image.StitchedImageTemplate; +import com.dfsek.terra.addons.image.config.noisesampler.CellularImageSamplerTemplate; import com.dfsek.terra.addons.image.config.noisesampler.ChannelNoiseSamplerTemplate; import com.dfsek.terra.addons.image.config.noisesampler.DistanceTransformNoiseSamplerTemplate; import com.dfsek.terra.addons.image.image.Image; @@ -75,6 +76,7 @@ public void initialize() { NOISE_SAMPLER_TOKEN); noiseRegistry.register(addon.key("DISTANCE_TRANSFORM"), DistanceTransformNoiseSamplerTemplate::new); noiseRegistry.register(addon.key("CHANNEL"), ChannelNoiseSamplerTemplate::new); + noiseRegistry.register(addon.key("CELLULAR_IMAGE"), CellularImageSamplerTemplate::new); }) .then(event -> { CheckedRegistry>> colorSamplerRegistry = event.getPack().getOrCreateRegistry( diff --git a/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/config/noisesampler/CellularImageSamplerTemplate.java b/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/config/noisesampler/CellularImageSamplerTemplate.java new file mode 100644 index 0000000000..ecbe9f6d7c --- /dev/null +++ b/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/config/noisesampler/CellularImageSamplerTemplate.java @@ -0,0 +1,54 @@ +package com.dfsek.terra.addons.image.config.noisesampler; + +import com.dfsek.tectonic.api.config.template.annotations.Default; +import com.dfsek.tectonic.api.config.template.annotations.Value; +import com.dfsek.tectonic.api.config.template.object.ObjectTemplate; +import com.dfsek.terra.addons.image.colorsampler.image.transform.Alignment; +import com.dfsek.terra.addons.image.image.Image; +import com.dfsek.terra.addons.image.noisesampler.CellularImageSampler; +import com.dfsek.terra.api.config.meta.Meta; +import com.dfsek.terra.api.noise.CellularDistanceFunction; +import com.dfsek.terra.api.noise.CellularReturnType; +import com.dfsek.terra.api.noise.NoiseSampler; + +public class CellularImageSamplerTemplate implements ObjectTemplate { + + @Value("image") + private Image image; + + @Value("distance") + @Default + private @Meta CellularDistanceFunction cellularDistanceFunction = CellularDistanceFunction.EuclideanSq; + + @Value("return") + @Default + private CellularReturnType cellularReturnType = CellularReturnType.Distance; + + @Value("lookup") + @Default + private @Meta NoiseSampler lookup; + + @Value("align") + @Default + private @Meta Alignment align; + + @Value("hash") + @Default + private @Meta String hash = ""; + + @Override + public NoiseSampler get() { + CellularImageSampler sampler = new CellularImageSampler(); + sampler.setImage(image); + sampler.setReturnType(cellularReturnType); + sampler.setDistanceFunction(cellularDistanceFunction); + sampler.setNoiseLookup(lookup); + sampler.setAlignment(align); + sampler.setHash(hash); + if(!sampler.hasTree(hash)){ + sampler.doKDTree(); + } + return sampler; + } +} + diff --git a/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/noisesampler/CellularImageSampler.java b/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/noisesampler/CellularImageSampler.java new file mode 100644 index 0000000000..281f190b67 --- /dev/null +++ b/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/noisesampler/CellularImageSampler.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2020-2025 Polyhedral Development + * + * The Terra Core Addons are licensed under the terms of the MIT License. For more details, + * reference the LICENSE file in this module's root directory. + */ + +package com.dfsek.terra.addons.image.noisesampler; + +import com.dfsek.terra.addons.image.colorsampler.image.transform.Alignment; +import com.dfsek.terra.addons.image.image.Image; +import com.dfsek.terra.addons.image.util.KDTree; +import com.dfsek.terra.api.noise.CellularDistanceFunction; +import com.dfsek.terra.api.noise.CellularReturnType; +import com.dfsek.terra.api.noise.NoiseSampler; +import com.dfsek.terra.api.util.vector.Vector2; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + + +/** + * NoiseSampler implementation for A Modified Cellular (Voronoi/Worley) Noise with Image Sampling for Seeding White pixels #FFFFFF being seeds + */ +public class CellularImageSampler implements NoiseSampler { + private CellularDistanceFunction distanceFunction = CellularDistanceFunction.EuclideanSq; + private CellularReturnType returnType = CellularReturnType.Distance; + private NoiseSampler noiseLookup; + private Image image; + private KDTree tree; + private Alignment alignment = Alignment.NONE; + private static Map treeMap = new ConcurrentHashMap<>(); + private int hash; + + + public void setDistanceFunction(CellularDistanceFunction distanceFunction) { + this.distanceFunction = distanceFunction; + } + + public void setNoiseLookup(NoiseSampler noiseLookup) { + this.noiseLookup = noiseLookup; + } + + public void setReturnType(CellularReturnType returnType) { + this.returnType = returnType; + } + + public void setImage(Image image){ + this.image = image; + } + + public void setAlignment(Alignment alignment) { + this.alignment = alignment; + } + + private int getHash(String string){ + return Objects.hash(string); + } + + public void setHash(String string){ + this.hash = getHash(string); + } + + public boolean hasTree(String string){ + return treeMap.containsKey(getHash(string)); + } + + + public KDTree doKDTree() { + return treeMap.computeIfAbsent(hash, h -> { + List whitePixels = extractWhitePixels(image); + return new KDTree(whitePixels); + }); + } + + + public List extractWhitePixels(Image image) { + int width = image.getWidth(); + int height = image.getHeight(); + + int offsetX = alignment == Alignment.CENTER ? -width / 2 : 0; + int offsetZ = alignment == Alignment.CENTER ? -height / 2 : 0; + + return IntStream.range(0, height).parallel() + .boxed() + .flatMap(y -> + IntStream.range(0, width) + .filter(x -> (image.getRGB(x, y) & 0xFFFFFF) == 0xFFFFFF) + .mapToObj(x -> { + Vector2 v = Vector2.of(x + offsetX, y + offsetZ); + return v; + }) + ) + .collect(Collectors.toList()); + } + + + + @Override + public double noise(long sl, double x, double z) { + KDTree tree = treeMap.get(hash); + + int xr = (int) Math.round(x); + int zr = (int) Math.round(z); + Vector2 query = Vector2.of(xr, zr); + + List nearest = tree.kNearest(query, 3); + + double distance0, distance1, distance2; + + if (distanceFunction == CellularDistanceFunction.Manhattan) { + distance0 = Math.abs(query.getX() - nearest.get(0).getX()) + Math.abs(query.getZ() - nearest.get(0).getZ()); + distance1 = Math.abs(query.getX() - nearest.get(1).getX()) + Math.abs(query.getZ() - nearest.get(1).getZ()); + distance2 = Math.abs(query.getX() - nearest.get(2).getX()) + Math.abs(query.getZ() - nearest.get(2).getZ()); + } else { + distance0 = applyDistanceFunction(distanceFunction, query.distanceSquared(nearest.get(0))); + distance1 = applyDistanceFunction(distanceFunction, query.distanceSquared(nearest.get(1))); + distance2 = applyDistanceFunction(distanceFunction, query.distanceSquared(nearest.get(2))); + } + + double distanceX = nearest.get(0).getX(); + double distanceZ = nearest.get(0).getZ(); + + CellularReturnType type = returnType; + + double result = switch(type) { + case Distance -> distance0 - 1; + case Distance2 -> distance1 - 1; + case Distance2Add -> (distance1 + distance0) * 0.5 - 1; + case Distance2Sub -> distance1 - distance0 - 1; + case Distance2Mul -> distance1 * distance0 * 0.5 - 1; + case Distance2Div -> distance0 / distance1 - 1; + case NoiseLookup -> noiseLookup.noise(sl, distanceX, distanceZ); + case LocalNoiseLookup -> noiseLookup.noise(sl, x - distanceX, z - distanceZ); + case Distance3 -> distance2 - 1; + case Distance3Add -> (distance2 + distance0) * 0.5 - 1; + case Distance3Sub -> distance2 - distance0 - 1; + case Distance3Mul -> distance2 * distance0 - 1; + case Distance3Div -> distance0 / distance2 - 1; + case Angle -> Math.atan2(distanceX - x, distanceZ - z); + case CellValue -> hashNormalized((int) distanceX, (int) distanceZ); + }; + + return result; + } + + + private double hashNormalized(int x, int z) { + int h = x * 73428767 ^ z * 912367; + h ^= (h >>> 13); + h *= 0x85ebca6b; + h ^= (h >>> 16); + return (h & 0x7FFFFFFF) / (double) 0x7FFFFFFF * 2.0 - 1.0; + } + + private double applyDistanceFunction(CellularDistanceFunction function, double distSq) { + return switch (function) { + case Euclidean -> Math.sqrt(distSq); + case EuclideanSq -> distSq; + case Manhattan -> Math.sqrt(distSq) * 1.5; + case Hybrid -> Math.sqrt(distSq) + 0.25 * distSq; + }; + } + + @Override + public double noise(long seed, double x, double y, double z) { + return noise(seed, x, z); + } +} + diff --git a/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/util/KDTree.java b/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/util/KDTree.java new file mode 100644 index 0000000000..2b20cdc37f --- /dev/null +++ b/common/addons/library-image/src/main/java/com/dfsek/terra/addons/image/util/KDTree.java @@ -0,0 +1,112 @@ +package com.dfsek.terra.addons.image.util; + +import com.dfsek.terra.api.util.vector.Vector2; + +import java.util.*; + +public class KDTree { + + + private static class Node { + Vector2 point; + Node left, right; + int axis; + + Node(Vector2 point, int axis) { + this.point = point; + this.axis = axis; + } + } + + private Node root; + + public KDTree(List points) { + root = build(points, 0); + } + + private Node build(List pts, int depth) { + if (pts.isEmpty()) return null; + + int axis = depth % 2; + pts.sort(Comparator.comparingDouble(p -> axis == 0 ? p.getX() : p.getZ())); + int median = pts.size() / 2; + + Node node = new Node(pts.get(median), axis); + node.left = build(pts.subList(0, median), depth + 1); + node.right = build(pts.subList(median + 1, pts.size()), depth + 1); + return node; + } + + public Vector2 nearest(Vector2 target) { + return nearest(root, target, root.point, Double.POSITIVE_INFINITY); + } + + private Vector2 nearest(Node node, Vector2 target, Vector2 bestPoint, double bestDistSq) { + if (node == null) return bestPoint; + + double d = distSq(target, node.point); + if (d < bestDistSq) { + bestDistSq = d; + bestPoint = node.point; + } + + Node near = (getAxis(target, node.axis) < getAxis(node.point, node.axis)) ? node.left : node.right; + Node far = (near == node.left) ? node.right : node.left; + + bestPoint = nearest(near, target, bestPoint, bestDistSq); + bestDistSq = distSq(target, bestPoint); + + double axisDist = getAxis(target, node.axis) - getAxis(node.point, node.axis); + if (axisDist * axisDist < bestDistSq) { + bestPoint = nearest(far, target, bestPoint, bestDistSq); + } + + return bestPoint; + } + + public List kNearest(Vector2 target, int k) { + PriorityQueue best = new PriorityQueue<>(Comparator.comparingDouble(n -> -n.distSq)); + kNearest(root, target, k, best); + List sorted = new ArrayList<>(best); + sorted.sort(Comparator.comparingDouble(n -> n.distSq)); + + List result = new ArrayList<>(); + for (Neighbor n : sorted) result.add(n.point); + return result; + } + + + private void kNearest(Node node, Vector2 target, int k, PriorityQueue best) { + if (node == null) return; + + double dSq = distSq(target, node.point); + if (best.size() < k) { + best.add(new Neighbor(node.point, dSq)); + } else if (dSq < best.peek().distSq) { + best.poll(); + best.add(new Neighbor(node.point, dSq)); + } + + Node near = (getAxis(target, node.axis) < getAxis(node.point, node.axis)) ? node.left : node.right; + Node far = (near == node.left) ? node.right : node.left; + + kNearest(near, target, k, best); + + double axisDist = getAxis(target, node.axis) - getAxis(node.point, node.axis); + if (best.size() < k || axisDist * axisDist < best.peek().distSq) { + kNearest(far, target, k, best); + } + } + + private double distSq(Vector2 a, Vector2 b) { + double dx = a.getX() - b.getX(); + double dy = a.getZ() - b.getZ(); + return dx * dx + dy * dy; + } + + private double getAxis(Vector2 v, int axis) { + return axis == 0 ? v.getX() : v.getZ(); + } + + private record Neighbor(Vector2 point, double distSq) {} +} diff --git a/common/api/src/main/java/com/dfsek/terra/api/noise/CellularDistanceFunction.java b/common/api/src/main/java/com/dfsek/terra/api/noise/CellularDistanceFunction.java new file mode 100644 index 0000000000..f19ff88077 --- /dev/null +++ b/common/api/src/main/java/com/dfsek/terra/api/noise/CellularDistanceFunction.java @@ -0,0 +1,8 @@ +package com.dfsek.terra.api.noise; + +public enum CellularDistanceFunction { + Euclidean, + EuclideanSq, + Manhattan, + Hybrid +} diff --git a/common/api/src/main/java/com/dfsek/terra/api/noise/CellularReturnType.java b/common/api/src/main/java/com/dfsek/terra/api/noise/CellularReturnType.java new file mode 100644 index 0000000000..15090d3418 --- /dev/null +++ b/common/api/src/main/java/com/dfsek/terra/api/noise/CellularReturnType.java @@ -0,0 +1,19 @@ +package com.dfsek.terra.api.noise; + +public enum CellularReturnType { + CellValue, + Distance, + Distance2, + Distance2Add, + Distance2Sub, + Distance2Mul, + Distance2Div, + NoiseLookup, + LocalNoiseLookup, + Distance3, + Distance3Add, + Distance3Sub, + Distance3Mul, + Distance3Div, + Angle +}