-
Notifications
You must be signed in to change notification settings - Fork 144
Adding an Image-based Cellular Sampler for the Image-Library addon #520
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 11 commits
05d5f22
a458d78
22dc342
dd175aa
8e9a2b6
1d02c66
dc1eec7
0096aa2
4dfc0a3
fb7c302
35a551d
68452bf
d123bc9
23e0140
03d1b57
b838959
3a26ae8
9c17221
52dad2b
d16947e
b1ddc3c
b4f6efe
8dcef54
c761232
987e4d7
b48cc46
80917bd
e50abfd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| 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; | ||
|
ItsJuls marked this conversation as resolved.
|
||
| 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.NoiseSampler; | ||
|
|
||
|
|
||
| public class CellularImageSamplerTemplate implements ObjectTemplate<NoiseSampler> { | ||
|
|
||
| @Value("image") | ||
| private Image image; | ||
|
|
||
| @Value("distance") | ||
| @Default | ||
| private CellularImageSampler.@Meta DistanceFunction cellularDistanceFunction = CellularImageSampler.DistanceFunction.EuclideanSq; | ||
|
|
||
| @Value("return") | ||
| @Default | ||
| private CellularImageSampler.@Meta ReturnType cellularReturnType = CellularImageSampler.ReturnType.Distance; | ||
|
|
||
| @Value("lookup") | ||
| @Default | ||
| private @Meta NoiseSampler lookup; | ||
|
|
||
| @Value("align") | ||
| @Default | ||
| private @Meta Alignment align; | ||
|
|
||
| @Override | ||
| public NoiseSampler get() { | ||
| CellularImageSampler sampler = new CellularImageSampler(); | ||
| sampler.setImage(image); | ||
| sampler.setReturnType(cellularReturnType); | ||
| sampler.setDistanceFunction(cellularDistanceFunction); | ||
| sampler.setNoiseLookup(lookup); | ||
| sampler.setAlignment(align); | ||
| if(!sampler.isTreeSet()){ | ||
| sampler.doKDTree(); | ||
| } | ||
| return sampler; | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,214 @@ | ||
| /* | ||
| * 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.NoiseSampler; | ||
| import com.dfsek.terra.api.util.vector.Vector2; | ||
|
|
||
|
|
||
| import java.util.ArrayList; | ||
|
ItsJuls marked this conversation as resolved.
Outdated
|
||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Objects; | ||
| import java.util.concurrent.CompletableFuture; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
|
|
||
|
|
||
| /** | ||
| * 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 DistanceFunction distanceFunction = DistanceFunction.EuclideanSq; | ||
| private ReturnType returnType = ReturnType.Distance; | ||
| private NoiseSampler noiseLookup; | ||
| private Image image; | ||
| private KDTree tree; | ||
| private Alignment alignment = Alignment.NONE; | ||
| private static final Map<Integer, CompletableFuture<KDTree>> treeFutures = new ConcurrentHashMap<>(); | ||
|
|
||
|
|
||
|
|
||
| public void setDistanceFunction(DistanceFunction distanceFunction) { | ||
| this.distanceFunction = distanceFunction; | ||
| } | ||
|
|
||
| public void setNoiseLookup(NoiseSampler noiseLookup) { | ||
| this.noiseLookup = noiseLookup; | ||
| } | ||
|
|
||
| public void setReturnType(ReturnType returnType) { | ||
| this.returnType = returnType; | ||
| } | ||
|
|
||
| public void setImage(Image image){ | ||
| this.image = image; | ||
| } | ||
|
|
||
| public void setAlignment(Alignment alignment) { | ||
| this.alignment = alignment; | ||
| } | ||
|
|
||
| private int hash() { | ||
| return Objects.hash(alignment.name()); | ||
| } | ||
|
|
||
| public boolean isTreeSet() { | ||
| CompletableFuture<KDTree> future = treeFutures.get(hash()); | ||
| return future != null && future.isDone() && !future.isCompletedExceptionally(); | ||
| } | ||
|
|
||
| public void doKDTree() { | ||
| treeFutures.computeIfAbsent(hash(), h -> CompletableFuture.supplyAsync(() -> { | ||
| List<Vector2> whitePixels = extractWhitePixels(image); | ||
| return new KDTree(whitePixels); | ||
| })).thenAccept(tree -> { | ||
| this.tree = tree; | ||
| }); | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is kinda awful tbh why does there's also a probably race condition waiting to happen here.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've been trying to figure out how to only create the Tree once per image on pack load, as I've observed multiple instances of the sampler gets called which bogs down my server. This is jank rn and I'm trying to figure out what soluton can be done as I need some sort of check if the tree has been made already and maps to an image in an efficient way |
||
|
|
||
|
|
||
| public List<Vector2> extractWhitePixels(Image image) { | ||
| int width = image.getWidth(); | ||
| int height = image.getHeight(); | ||
|
|
||
| int offsetX = 0; | ||
| int offsetZ = 0; | ||
|
|
||
| if (alignment == Alignment.CENTER) { | ||
| offsetX = -width / 2; | ||
| offsetZ = -height / 2; | ||
| } | ||
|
|
||
| List<Vector2> points = new ArrayList<>(); | ||
|
|
||
| for (int y = 0; y < height; y++) { | ||
| for (int x = 0; x < width; x++) { | ||
| int rgb = image.getRGB(x, y) & 0xFFFFFF; | ||
| if (rgb == 0xFFFFFF) { | ||
| Vector2 point = Vector2.of(x + offsetX, y + offsetZ); | ||
| points.add(point); | ||
|
|
||
| } | ||
| } | ||
| } | ||
|
|
||
| return points; | ||
| } | ||
|
|
||
| @Override | ||
| public double noise(long sl, double x, double z) { | ||
| CompletableFuture<KDTree> future = treeFutures.get(hash()); | ||
|
|
||
| if (future == null || future.isCompletedExceptionally()) { | ||
| throw new IllegalStateException("KDTree not initialized for image."); | ||
| } | ||
|
|
||
| try { | ||
| tree = future.get(); | ||
| } catch (Exception e) { | ||
| throw new RuntimeException("Error retrieving KDTree", e); | ||
| } | ||
|
|
||
| int xr = (int) Math.round(x); | ||
| int zr = (int) Math.round(z); | ||
| Vector2 query = Vector2.of(xr, zr); | ||
|
|
||
| List<Vector2> nearest = tree.kNearest(query, 3); | ||
|
|
||
| double distance0, distance1, distance2; | ||
|
|
||
| if (distanceFunction == DistanceFunction.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(); | ||
|
|
||
| ReturnType type = returnType; | ||
|
|
||
|
|
||
|
|
||
| double result = switch(type) { | ||
|
ItsJuls marked this conversation as resolved.
Outdated
|
||
| 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(DistanceFunction 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); | ||
| } | ||
|
|
||
| public enum DistanceFunction { | ||
| Euclidean, | ||
| EuclideanSq, | ||
| Manhattan, | ||
| Hybrid | ||
| } | ||
|
|
||
| public enum ReturnType { | ||
| Distance, | ||
| Distance2, | ||
| Distance2Add, | ||
| Distance2Sub, | ||
| Distance2Mul, | ||
| Distance2Div, | ||
| NoiseLookup, | ||
| LocalNoiseLookup, | ||
| Distance3, | ||
| Distance3Add, | ||
| Distance3Sub, | ||
| Distance3Mul, | ||
| Distance3Div, | ||
| Angle, | ||
| CellValue | ||
| } | ||
|
ItsJuls marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.