mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-22 16:17:44 +00:00
Keep huge procedural trees from stalling
Faster leaf support; height capped at 128.
This commit is contained in:
1 parent
2659837007
commit
03fd5e4bf3
3 files changed
+175
-38
No files matched your search
@@ -33,6 +33,8 @@ public final class ResolvedTree extends ResolvedProceduralPlacement {
|
||||
private static final long VARIANT_SEED_STRIDE = 7919L;
|
||||
private static final double HEIGHT_JITTER_FRACTION = 0.3;
|
||||
private static final int MINIMUM_HEIGHT = 2;
|
||||
/** Nearly three times the tallest tree in the packs. Taller settings are clamped so a typo bakes in seconds, not hours. */
|
||||
static final int TALLEST_TREE_BLOCKS = 128;
|
||||
|
||||
private final Tree settings;
|
||||
|
||||
@@ -55,8 +57,8 @@ public final class ResolvedTree extends ResolvedProceduralPlacement {
|
||||
protected List<IrisObject> bake(ObjectBuilder blocks) {
|
||||
List<IrisObject> baked = new ArrayList<>();
|
||||
int count = Math.max(1, settings.variants());
|
||||
int lo = Math.min(settings.heightMin(), settings.heightMax());
|
||||
int hi = Math.max(settings.heightMin(), settings.heightMax());
|
||||
int lo = Math.min(Math.min(settings.heightMin(), settings.heightMax()), TALLEST_TREE_BLOCKS);
|
||||
int hi = Math.min(Math.max(settings.heightMin(), settings.heightMax()), TALLEST_TREE_BLOCKS);
|
||||
RNG heightRng = new RNG(settings.seed());
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
|
||||
@@ -104,10 +104,12 @@ public final class Tree {
|
||||
private TreeProfile profile = TreeProfile.OAK;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(ResolvedTree.TALLEST_TREE_BLOCKS)
|
||||
@Desc("Minimum trunk height in blocks.")
|
||||
private int heightMin = 8;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(ResolvedTree.TALLEST_TREE_BLOCKS)
|
||||
@Desc("Maximum trunk height in blocks.")
|
||||
private int heightMax = 12;
|
||||
|
||||
|
||||
+169
-36
@@ -19,7 +19,10 @@
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class TreeSupport {
|
||||
private static final int LEGAL_DISTANCE = 6;
|
||||
@@ -28,46 +31,65 @@ public final class TreeSupport {
|
||||
private TreeSupport() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Grows wood toward the leaves farthest from any, one tendril at a time, until every leaf is close enough to wood
|
||||
* not to decay or {@code maxTendrils} have grown. Wood only ever gets added, so each leaf's nearest wood is found
|
||||
* once and afterwards only compared against the newest tendril.
|
||||
*/
|
||||
public static void ensureLeavesSupported(TreeBlockCanvas canvas, int maxTendrils) {
|
||||
for (int iter = 0; iter < maxTendrils; iter++) {
|
||||
Map<TreeBlockCanvas.Vec, Integer> dist = TreePlausibility.computeDistances(canvas.getTrunk(), canvas.getLeaf());
|
||||
|
||||
TreeBlockCanvas.Vec worstLeaf = null;
|
||||
TreeBlockCanvas.Vec worstWood = null;
|
||||
int worstGap = -1;
|
||||
|
||||
for (TreeBlockCanvas.Vec leaf : new ArrayList<>(canvas.getLeaf())) {
|
||||
Integer d = dist.get(leaf);
|
||||
if (d != null && d <= LEGAL_DISTANCE) {
|
||||
continue;
|
||||
}
|
||||
TreeBlockCanvas.Vec nearest = null;
|
||||
int best = Integer.MAX_VALUE;
|
||||
for (TreeBlockCanvas.Vec wood : canvas.getTrunk()) {
|
||||
int cd = chebyshev(leaf, wood);
|
||||
if (cd < best) {
|
||||
best = cd;
|
||||
nearest = wood;
|
||||
if (cd <= 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nearest != null && best > worstGap) {
|
||||
worstGap = best;
|
||||
worstLeaf = leaf;
|
||||
worstWood = nearest;
|
||||
}
|
||||
}
|
||||
|
||||
if (worstLeaf == null) {
|
||||
return;
|
||||
}
|
||||
growTendril(canvas, worstWood, worstLeaf);
|
||||
if (canvas.getTrunk().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<TreeBlockCanvas.Vec> unsupported = unsupportedLeaves(canvas);
|
||||
Map<TreeBlockCanvas.Vec, Nearest> nearestWoodByLeaf = NearestWood.of(canvas.getTrunk()).nearestTo(unsupported);
|
||||
for (int tendril = 0; tendril < maxTendrils && !unsupported.isEmpty(); tendril++) {
|
||||
TreeBlockCanvas.Vec farthestLeaf = farthestFromWood(unsupported, nearestWoodByLeaf);
|
||||
List<TreeBlockCanvas.Vec> tendrilWood = growTendril(canvas, nearestWoodByLeaf.get(farthestLeaf).wood(), farthestLeaf);
|
||||
unsupported = unsupportedLeaves(canvas);
|
||||
moveTowardNewWood(nearestWoodByLeaf, unsupported, tendrilWood);
|
||||
}
|
||||
}
|
||||
|
||||
private static void growTendril(TreeBlockCanvas canvas, TreeBlockCanvas.Vec from, TreeBlockCanvas.Vec to) {
|
||||
private static List<TreeBlockCanvas.Vec> unsupportedLeaves(TreeBlockCanvas canvas) {
|
||||
Map<TreeBlockCanvas.Vec, Integer> distance = TreePlausibility.computeDistances(canvas.getTrunk(), canvas.getLeaf());
|
||||
List<TreeBlockCanvas.Vec> unsupported = new ArrayList<>();
|
||||
for (TreeBlockCanvas.Vec leaf : canvas.getLeaf()) {
|
||||
Integer d = distance.get(leaf);
|
||||
if (d == null || d > LEGAL_DISTANCE) {
|
||||
unsupported.add(leaf);
|
||||
}
|
||||
}
|
||||
return unsupported;
|
||||
}
|
||||
|
||||
private static TreeBlockCanvas.Vec farthestFromWood(List<TreeBlockCanvas.Vec> leaves, Map<TreeBlockCanvas.Vec, Nearest> nearestWoodByLeaf) {
|
||||
TreeBlockCanvas.Vec farthest = null;
|
||||
int farthestGap = -1;
|
||||
for (TreeBlockCanvas.Vec leaf : leaves) {
|
||||
int gap = nearestWoodByLeaf.get(leaf).distance();
|
||||
if (gap > farthestGap) {
|
||||
farthestGap = gap;
|
||||
farthest = leaf;
|
||||
}
|
||||
}
|
||||
return farthest;
|
||||
}
|
||||
|
||||
private static void moveTowardNewWood(Map<TreeBlockCanvas.Vec, Nearest> nearestWoodByLeaf, List<TreeBlockCanvas.Vec> leaves,
|
||||
List<TreeBlockCanvas.Vec> newWood) {
|
||||
for (TreeBlockCanvas.Vec leaf : leaves) {
|
||||
Nearest nearest = nearestWoodByLeaf.get(leaf);
|
||||
for (TreeBlockCanvas.Vec wood : newWood) {
|
||||
int distance = chebyshev(leaf, wood);
|
||||
if (distance < nearest.distance()) {
|
||||
nearest = new Nearest(wood, distance);
|
||||
}
|
||||
}
|
||||
nearestWoodByLeaf.put(leaf, nearest);
|
||||
}
|
||||
}
|
||||
|
||||
private static List<TreeBlockCanvas.Vec> growTendril(TreeBlockCanvas canvas, TreeBlockCanvas.Vec from, TreeBlockCanvas.Vec to) {
|
||||
int dx = to.x() - from.x();
|
||||
int dy = to.y() - from.y();
|
||||
int dz = to.z() - from.z();
|
||||
@@ -75,16 +97,127 @@ public final class TreeSupport {
|
||||
int steps = Math.max(1, length - REACH_GAP);
|
||||
TreeBlockCanvas.Axis axis = TreeFunctions.logAxis(dx, dy, dz);
|
||||
|
||||
List<TreeBlockCanvas.Vec> grown = new ArrayList<>(steps);
|
||||
for (int i = 1; i <= steps; i++) {
|
||||
double t = i / (double) length;
|
||||
int x = from.x() + (int) Math.round(dx * t);
|
||||
int y = from.y() + (int) Math.round(dy * t);
|
||||
int z = from.z() + (int) Math.round(dz * t);
|
||||
canvas.setTrunk(x, y, z, TreeBlockCanvas.Role.TRUNK, axis);
|
||||
grown.add(new TreeBlockCanvas.Vec(x, y, z));
|
||||
}
|
||||
return grown;
|
||||
}
|
||||
|
||||
private static int chebyshev(TreeBlockCanvas.Vec a, TreeBlockCanvas.Vec b) {
|
||||
return Math.max(Math.max(Math.abs(a.x() - b.x()), Math.abs(a.y() - b.y())), Math.abs(a.z() - b.z()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the closest wood block to a leaf, by the largest of the x, y and z gaps, searching outward through a coarse
|
||||
* grid of wood one shell of buckets at a time. Ties go to the wood that comes first in the trunk, as a plain scan
|
||||
* would pick.
|
||||
*/
|
||||
private static final class NearestWood {
|
||||
private static final int BUCKET_SIZE_BLOCKS = 8;
|
||||
|
||||
private final Map<Long, List<Integer>> woodIndicesByBucket = new HashMap<>();
|
||||
private final TreeBlockCanvas.Vec[] wood;
|
||||
private int lowestBucket = Integer.MAX_VALUE;
|
||||
private int highestBucket = Integer.MIN_VALUE;
|
||||
|
||||
private NearestWood(TreeBlockCanvas.Vec[] wood) {
|
||||
this.wood = wood;
|
||||
for (int index = 0; index < wood.length; index++) {
|
||||
TreeBlockCanvas.Vec block = wood[index];
|
||||
woodIndicesByBucket.computeIfAbsent(bucketKey(bucket(block.x()), bucket(block.y()), bucket(block.z())), ignored -> new ArrayList<>()).add(index);
|
||||
for (int coordinate : new int[]{block.x(), block.y(), block.z()}) {
|
||||
lowestBucket = Math.min(lowestBucket, bucket(coordinate));
|
||||
highestBucket = Math.max(highestBucket, bucket(coordinate));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static NearestWood of(Set<TreeBlockCanvas.Vec> trunk) {
|
||||
return new NearestWood(trunk.toArray(TreeBlockCanvas.Vec[]::new));
|
||||
}
|
||||
|
||||
Map<TreeBlockCanvas.Vec, Nearest> nearestTo(List<TreeBlockCanvas.Vec> leaves) {
|
||||
Map<TreeBlockCanvas.Vec, Nearest> nearestByLeaf = new HashMap<>();
|
||||
for (TreeBlockCanvas.Vec leaf : leaves) {
|
||||
nearestByLeaf.put(leaf, nearestTo(leaf));
|
||||
}
|
||||
return nearestByLeaf;
|
||||
}
|
||||
|
||||
private Nearest nearestTo(TreeBlockCanvas.Vec leaf) {
|
||||
int leafBucketX = bucket(leaf.x());
|
||||
int leafBucketY = bucket(leaf.y());
|
||||
int leafBucketZ = bucket(leaf.z());
|
||||
Search search = new Search(leaf);
|
||||
int lastRing = ringReachingAllWood(leafBucketX, leafBucketY, leafBucketZ);
|
||||
for (int ring = 0; ring <= lastRing; ring++) {
|
||||
for (int dx = -ring; dx <= ring; dx++) {
|
||||
for (int dy = -ring; dy <= ring; dy++) {
|
||||
boolean onShellAlready = Math.abs(dx) == ring || Math.abs(dy) == ring;
|
||||
int dzStep = onShellAlready || ring == 0 ? 1 : 2 * ring;
|
||||
for (int dz = -ring; dz <= ring; dz += dzStep) {
|
||||
search.consider(woodIndicesByBucket.get(bucketKey(leafBucketX + dx, leafBucketY + dy, leafBucketZ + dz)));
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean fartherRingsCannotBeCloser = search.found() && search.bestDistance <= ring * BUCKET_SIZE_BLOCKS;
|
||||
if (fartherRingsCannotBeCloser) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new Nearest(wood[search.bestIndex], search.bestDistance);
|
||||
}
|
||||
|
||||
private int ringReachingAllWood(int... leafBuckets) {
|
||||
int ring = 0;
|
||||
for (int leafBucket : leafBuckets) {
|
||||
ring = Math.max(ring, Math.max(Math.abs(leafBucket - lowestBucket), Math.abs(leafBucket - highestBucket)));
|
||||
}
|
||||
return ring;
|
||||
}
|
||||
|
||||
private static int bucket(int coordinate) {
|
||||
return Math.floorDiv(coordinate, BUCKET_SIZE_BLOCKS);
|
||||
}
|
||||
|
||||
private static long bucketKey(int x, int y, int z) {
|
||||
return ((long) x & 0x1FFFFF) | (((long) y & 0x1FFFFF) << 21) | (((long) z & 0x1FFFFF) << 42);
|
||||
}
|
||||
|
||||
private final class Search {
|
||||
private final TreeBlockCanvas.Vec leaf;
|
||||
private int bestDistance = Integer.MAX_VALUE;
|
||||
private int bestIndex = -1;
|
||||
|
||||
private Search(TreeBlockCanvas.Vec leaf) {
|
||||
this.leaf = leaf;
|
||||
}
|
||||
|
||||
void consider(List<Integer> woodIndices) {
|
||||
if (woodIndices == null) {
|
||||
return;
|
||||
}
|
||||
for (int index : woodIndices) {
|
||||
int distance = chebyshev(leaf, wood[index]);
|
||||
if (distance < bestDistance || (distance == bestDistance && index < bestIndex)) {
|
||||
bestDistance = distance;
|
||||
bestIndex = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean found() {
|
||||
return bestIndex >= 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record Nearest(TreeBlockCanvas.Vec wood, int distance) {
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user