Simplified procedural feature generation

- Fungi build stem roles directly without intermediate maps or coordinate arrays
- Tree palettes reuse one random source per object
- Persistent leaves skip distance traversal when plausibility is disabled
- Tree support scans avoid copied coordinate sets
- Coral arm samples reuse their endpoint array
- Sea decorators share scaled stack limits and respect output bounds
- Unresolved sea floor palette entries preserve existing water
- Tests cover stem geometry persistent leaves and sea stack limits
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-06 03:31:54 -04:00
1 parent ebaa616536
commit b9b969f1f9
14 files changed
+245 -79

No files matched your search

@@ -474,7 +474,7 @@ final class DecoratorCore {
&& (!caveSkipFluid || !B.isFluid(lower) && !B.isFluid(upper));
}
private static int computeStack(IrisDecorator decorator, RNG rng, double realX, double realZ,
static int computeStack(IrisDecorator decorator, RNG rng, double realX, double realZ,
IrisData irisData, int max) {
int stack = decorator.getHeight(rng, realX, realZ, irisData);
if (decorator.isScaleStack()) {
@@ -39,6 +39,10 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator {
@Override
public void decorate(int x, int z, int realX, int realX1, int realX_1, int realZ, int realZ1, int realZ_1,
Hunk<PlatformBlockState> data, IrisBiome biome, int height, int max) {
int limit = Math.min(max, Math.min(getEngine().getHeight(), data.getHeight()));
if (height < 0 || height >= limit) {
return;
}
RNG rng = getRNG(realX, realZ);
IrisDecorator decorator = DecoratorCore.pickDecorator(biome, getPart(), partRNG, rng, getData(), realX, realZ);
@@ -51,34 +55,25 @@ public class IrisSeaFloorDecorator extends IrisEngineDecorator {
&& !decorator.getSlopeCondition().isValid(getComplex().getSlopeStream().get(realX, realZ))) {
return;
}
if (height >= 0 && height < getEngine().getHeight()) {
data.set(x, height, z, decorator.getBlockData100(biome, rng, realX, height, realZ, getData()));
PlatformBlockState block = decorator.getBlockData100(biome, rng, realX, height, realZ, getData());
if (block != null) {
data.set(x, height, z, block);
}
return;
}
int stack = decorator.getHeight(rng, realX, realZ, getData());
if (decorator.isScaleStack()) {
stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
} else {
stack = Math.min(stack, max - height);
}
if (stack == 1) {
data.set(x, height, z, decorator.getBlockDataForTop(biome, rng, realX, height, realZ, getData()));
return;
}
int engineHeight = getEngine().getHeight();
int stack = Math.min(limit - height,
DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height));
for (int i = 0; i < stack; i++) {
int h = height + i;
if (h > max || h > engineHeight) {
continue;
}
double threshold = ((double) i) / (stack - 1);
data.set(x, h, z, threshold >= decorator.getTopThreshold()
double threshold = stack == 1 ? 1.0 : ((double) i) / (stack - 1);
PlatformBlockState block = threshold >= decorator.getTopThreshold()
? decorator.getBlockDataForTop(biome, rng, realX, h, realZ, getData())
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData()));
: decorator.getBlockData100(biome, rng, realX, h, realZ, getData());
if (block == null) {
break;
}
data.set(x, h, z, block);
}
}
}
@@ -58,10 +58,7 @@ public class IrisSeaSurfaceDecorator extends IrisEngineDecorator {
return;
}
int stack = decorator.getHeight(rng, realX, realZ, getData());
if (decorator.isScaleStack()) {
stack = (int) Math.ceil((double) (max - height) * ((double) stack / 100));
}
int stack = DecoratorCore.computeStack(decorator, rng, realX, realZ, getData(), max - height);
if (stack == 1) {
int targetY = height + 1;
@@ -170,7 +170,9 @@ public final class CoralGenerator {
int y = (int) Math.round(oy + dy * t);
int z = (int) Math.round(oz + dz * t - sway);
canvas.set(x, y, z, CoralCanvas.Role.STRUCTURE);
tip = new double[]{x, y, z};
tip[0] = x;
tip[1] = y;
tip[2] = z;
}
return tip;
}
@@ -63,11 +63,7 @@ public final class FungusGenerator {
private static void buildUpright(Map<Vector3i, FungusCellRole> roles, IrisFungus fungus, int height, RNG rng, long baseSeed) {
int stemHeight = Math.max(1, height);
Map<Vector3i, PlatformBlockState> stemCells = new HashMap<>();
double[] top = FungusStemBuilder.build(stemCells, fungus, stemHeight, baseSeed);
for (Vector3i v : stemCells.keySet()) {
roles.put(v, FungusCellRole.STEM);
}
double[] top = FungusStemBuilder.build(roles, fungus, stemHeight, baseSeed);
int radius = pickRadius(fungus, rng);
double cx = top[0];
@@ -20,7 +20,6 @@ package art.arcane.iris.engine.object.fungi;
import art.arcane.iris.engine.object.IrisFungus;
import art.arcane.iris.engine.object.tree.TreeFunctions;
import art.arcane.iris.spi.PlatformBlockState;
import art.arcane.iris.util.common.math.Vector3i;
import java.util.Map;
@@ -29,7 +28,7 @@ public final class FungusStemBuilder {
private FungusStemBuilder() {
}
public static double[] build(Map<Vector3i, PlatformBlockState> stemCells, IrisFungus fungus, int stemHeight, long seed) {
public static double[] build(Map<Vector3i, FungusCellRole> roles, IrisFungus fungus, int stemHeight, long seed) {
int width = Math.max(1, Math.min(3, fungus.getStemWidth()));
double maxLean = stemHeight * Math.tan(Math.toRadians(Math.max(0.0, fungus.getStemCurve())));
double leanRad = Math.toRadians(fungus.getStemLeanAzimuth());
@@ -40,6 +39,7 @@ public final class FungusStemBuilder {
double waveAzimuth = Math.toRadians(fungus.getStemLeanAzimuth() + 90.0);
double waveX = Math.sin(waveAzimuth);
double waveZ = Math.cos(waveAzimuth);
double wavePhase = TreeFunctions.valueNoise1D(seed, seed) * Math.PI * 2.0;
double topCx = 0.0;
double topCz = 0.0;
@@ -48,12 +48,16 @@ public final class FungusStemBuilder {
for (int y = 0; y < stemHeight; y++) {
double t = stemHeight <= 1 ? 0.0 : y / (double) (stemHeight - 1);
double lean = maxLean * (t * t);
double wave = waveAmp * Math.sin(2.0 * Math.PI * wavePeriods * t + TreeFunctions.valueNoise1D(seed, seed) * Math.PI * 2.0);
double wave = waveAmp * Math.sin(2.0 * Math.PI * wavePeriods * t + wavePhase);
double cx = lean * leanX + wave * waveX;
double cz = lean * leanZ + wave * waveZ;
for (int[] xz : squarePositions(cx, cz, width)) {
stemCells.put(new Vector3i(xz[0], y, xz[1]), null);
int ox = (width % 2 == 1 ? (int) Math.round(cx) : (int) Math.floor(cx) + 1) - width / 2;
int oz = (width % 2 == 1 ? (int) Math.round(cz) : (int) Math.floor(cz) + 1) - width / 2;
for (int dx = 0; dx < width; dx++) {
for (int dz = 0; dz < width; dz++) {
roles.put(new Vector3i(ox + dx, y, oz + dz), FungusCellRole.STEM);
}
}
topCx = cx;
@@ -62,34 +66,4 @@ public final class FungusStemBuilder {
return new double[]{topCx, topY, topCz};
}
static int[][] squarePositions(double cx, double cz, int width) {
if (width % 2 == 1) {
int half = width / 2;
int icx = (int) Math.round(cx);
int icz = (int) Math.round(cz);
int[][] out = new int[width * width][2];
int idx = 0;
for (int dx = -half; dx <= half; dx++) {
for (int dz = -half; dz <= half; dz++) {
out[idx][0] = icx + dx;
out[idx][1] = icz + dz;
idx++;
}
}
return out;
}
int ox = (int) Math.floor(cx) - width / 2 + 1;
int oz = (int) Math.floor(cz) - width / 2 + 1;
int[][] out = new int[width * width][2];
int idx = 0;
for (int dx = 0; dx < width; dx++) {
for (int dz = 0; dz < width; dz++) {
out[idx][0] = ox + dx;
out[idx][1] = oz + dz;
idx++;
}
}
return out;
}
}
@@ -66,9 +66,10 @@ public final class ProceduralTreeGenerator {
Map<TreeBlockCanvas.Vec, PlatformBlockState> resolved = new HashMap<>();
Set<TreeBlockCanvas.Vec> trunkPositions = new HashSet<>();
Set<TreeBlockCanvas.Vec> leafPositions = new HashSet<>();
RNG paletteRng = new RNG(tree.getSeed());
for (Map.Entry<TreeBlockCanvas.Vec, TreeBlockCanvas.Cell> entry : canvas.getCells().entrySet()) {
TreeBlockCanvas.Cell cell = entry.getValue();
PlatformBlockState state = TreeBlockResolver.resolve(tree, data, cell, entry.getKey());
PlatformBlockState state = TreeBlockResolver.resolve(tree, data, cell, entry.getKey(), paletteRng);
if (state == null) {
continue;
}
@@ -31,10 +31,7 @@ public final class TreeBlockResolver {
private TreeBlockResolver() {
}
public static PlatformBlockState resolve(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Cell cell, TreeBlockCanvas.Vec pos) {
RNG paletteRng = new RNG(tree.getSeed());
RNG posRng = new RNG(tree.getSeed() ^ positionHash(pos));
public static PlatformBlockState resolve(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Cell cell, TreeBlockCanvas.Vec pos, RNG paletteRng) {
switch (cell.role()) {
case TRUNK -> {
PlatformBlockState state = resolveBlock(tree.getTrunk(), tree.getTrunkPalette(), data, pos, paletteRng);
@@ -51,7 +48,7 @@ public final class TreeBlockResolver {
return resolveBlock(tree.getLeaves(), tree.getLeavesPalette(), data, pos, paletteRng);
}
case SECONDARY_LEAF -> {
PlatformBlockState state = resolveSecondaryLeaf(tree, data, pos, paletteRng, posRng);
PlatformBlockState state = resolveSecondaryLeaf(tree, data, pos, paletteRng);
if (state == null) {
state = resolveBlock(tree.getLeaves(), tree.getLeavesPalette(), data, pos, paletteRng);
}
@@ -94,12 +91,12 @@ public final class TreeBlockResolver {
return state;
}
private static PlatformBlockState resolveSecondaryLeaf(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Vec pos, RNG paletteRng, RNG posRng) {
private static PlatformBlockState resolveSecondaryLeaf(IrisProceduralTree tree, IrisData data, TreeBlockCanvas.Vec pos, RNG paletteRng) {
if (TreeTrunkBuilder.paletteSet(tree.getSecondaryLeavesPalette())) {
return tree.getSecondaryLeavesPalette().get(paletteRng, pos.x(), pos.y(), pos.z(), data);
}
if (tree.getWeightedSecondaryLeaves() != null && !tree.getWeightedSecondaryLeaves().isEmpty()) {
String picked = pickWeighted(tree, posRng);
String picked = pickWeighted(tree, new RNG(tree.getSeed() ^ positionHash(pos)));
return picked == null ? null : B.getStateOrNull(picked, false);
}
if (tree.getSecondaryLeaves() != null && !tree.getSecondaryLeaves().isEmpty()) {
@@ -50,7 +50,9 @@ public final class TreePlausibility {
return;
}
Map<TreeBlockCanvas.Vec, Integer> distance = computeDistances(trunkPositions, realLeaves);
Map<TreeBlockCanvas.Vec, Integer> distance = tree.isPlausible()
? computeDistances(trunkPositions, realLeaves)
: Map.of();
for (TreeBlockCanvas.Vec v : realLeaves) {
PlatformBlockState leaf = resolved.get(v);
@@ -18,7 +18,6 @@
package art.arcane.iris.engine.object.tree;
import java.util.ArrayList;
import java.util.Map;
public final class TreeSupport {
@@ -36,7 +35,7 @@ public final class TreeSupport {
TreeBlockCanvas.Vec worstWood = null;
int worstGap = -1;
for (TreeBlockCanvas.Vec leaf : new ArrayList<>(canvas.getLeaf())) {
for (TreeBlockCanvas.Vec leaf : canvas.getLeaf()) {
Integer d = dist.get(leaf);
if (d != null && d <= LEGAL_DISTANCE) {
continue;
@@ -121,7 +121,7 @@ public final class TreeTrunkBuilder {
}
private static void markExposedEnds(TreeBlockCanvas canvas) {
for (TreeBlockCanvas.Vec v : new ArrayList<>(canvas.getTrunk())) {
for (TreeBlockCanvas.Vec v : canvas.getTrunk()) {
if (!canvas.has(v.x(), v.y() + 1, v.z()) || !canvas.has(v.x(), v.y() - 1, v.z())) {
canvas.markExposed(v.x(), v.y(), v.z());
}