From 03fd5e4bf3804e91e3829aed4ef2849b3d019450 Mon Sep 17 00:00:00 2001 From: piexel Date: Tue, 20 Oct 2026 14:03:41 +0200 Subject: [PATCH] Keep huge procedural trees from stalling Faster leaf support; height capped at 128. --- .../object/procedural/tree/ResolvedTree.java | 6 +- .../engine/object/procedural/tree/Tree.java | 2 + .../object/procedural/tree/TreeSupport.java | 205 +++++++++++++++--- 3 files changed, 175 insertions(+), 38 deletions(-) diff --git a/core/src/main/java/com/volmit/iris/engine/object/procedural/tree/ResolvedTree.java b/core/src/main/java/com/volmit/iris/engine/object/procedural/tree/ResolvedTree.java index cae87a7a2..e368e4d0d 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/procedural/tree/ResolvedTree.java +++ b/core/src/main/java/com/volmit/iris/engine/object/procedural/tree/ResolvedTree.java @@ -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 bake(ObjectBuilder blocks) { List 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++) { diff --git a/core/src/main/java/com/volmit/iris/engine/object/procedural/tree/Tree.java b/core/src/main/java/com/volmit/iris/engine/object/procedural/tree/Tree.java index 285fc41f2..5765ac26f 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/procedural/tree/Tree.java +++ b/core/src/main/java/com/volmit/iris/engine/object/procedural/tree/Tree.java @@ -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; diff --git a/core/src/main/java/com/volmit/iris/engine/object/procedural/tree/TreeSupport.java b/core/src/main/java/com/volmit/iris/engine/object/procedural/tree/TreeSupport.java index ec002b945..462409339 100644 --- a/core/src/main/java/com/volmit/iris/engine/object/procedural/tree/TreeSupport.java +++ b/core/src/main/java/com/volmit/iris/engine/object/procedural/tree/TreeSupport.java @@ -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 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 unsupported = unsupportedLeaves(canvas); + Map nearestWoodByLeaf = NearestWood.of(canvas.getTrunk()).nearestTo(unsupported); + for (int tendril = 0; tendril < maxTendrils && !unsupported.isEmpty(); tendril++) { + TreeBlockCanvas.Vec farthestLeaf = farthestFromWood(unsupported, nearestWoodByLeaf); + List 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 unsupportedLeaves(TreeBlockCanvas canvas) { + Map distance = TreePlausibility.computeDistances(canvas.getTrunk(), canvas.getLeaf()); + List 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 leaves, Map 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 nearestWoodByLeaf, List leaves, + List 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 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 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> 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 trunk) { + return new NearestWood(trunk.toArray(TreeBlockCanvas.Vec[]::new)); + } + + Map nearestTo(List leaves) { + Map 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 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) { + } }