mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-22 08:07:42 +00:00
Port procedural objects from v4
Shown in the Objects view.
This commit is contained in:
1 parent
59626d2434
commit
26825869e9
81 files changed
+8890
-5
No files matched your search
@@ -19,8 +19,10 @@
|
||||
package com.volmit.iris.core.gui;
|
||||
|
||||
import com.volmit.iris.core.gui.BlockTextures.BlockTexture;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.object.IrisObject;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralObjectCatalog;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import org.bukkit.util.BlockVector;
|
||||
@@ -29,6 +31,7 @@ import javax.swing.JComponent;
|
||||
import java.awt.EventQueue;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -140,7 +143,10 @@ final class ObjectExplorer {
|
||||
}
|
||||
|
||||
private List<String> objectKeys() {
|
||||
return Arrays.asList(engine().getData().getObjectLoader().getPossibleKeys());
|
||||
IrisData data = engine().getData();
|
||||
List<String> keys = new ArrayList<>(Arrays.asList(data.getObjectLoader().getPossibleKeys()));
|
||||
keys.addAll(data.getProceduralObjects().variantKeys());
|
||||
return keys;
|
||||
}
|
||||
|
||||
private void showCatalog(List<String> keys) {
|
||||
@@ -179,9 +185,10 @@ final class ObjectExplorer {
|
||||
}
|
||||
|
||||
private LoadedObject load(String key) {
|
||||
IrisObject object = engine().getData().getObjectLoader().load(key);
|
||||
IrisObject object = objectFor(key);
|
||||
if (object == null) {
|
||||
throw new IllegalStateException("The object file could not be read");
|
||||
throw new IllegalStateException(ProceduralObjectCatalog.isVariantKey(key)
|
||||
? "This variant has no blocks" : "The object file could not be read");
|
||||
}
|
||||
Map<BlockVector, BlockData> blocks = new HashMap<>();
|
||||
object.getBlocks().forEach((position, block) -> {
|
||||
@@ -194,6 +201,14 @@ final class ObjectExplorer {
|
||||
return new LoadedObject(blocks, mesh, summary);
|
||||
}
|
||||
|
||||
private IrisObject objectFor(String key) {
|
||||
IrisData data = engine().getData();
|
||||
if (ProceduralObjectCatalog.isVariantKey(key)) {
|
||||
return data.getProceduralObjects().variant(key);
|
||||
}
|
||||
return data.getObjectLoader().load(key);
|
||||
}
|
||||
|
||||
private void showLoaded(long revision, LoadedObject loaded) {
|
||||
if (revision != objectRevision.get()) {
|
||||
return;
|
||||
|
||||
@@ -30,6 +30,7 @@ import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.object.*;
|
||||
import com.volmit.iris.engine.object.annotations.Snippet;
|
||||
import com.volmit.iris.engine.object.matter.IrisMatterObject;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralObjectCatalog;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.collection.KMap;
|
||||
import com.volmit.iris.util.context.IrisContext;
|
||||
@@ -84,6 +85,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
private ResourceLoader<IrisRavine> ravineLoader;
|
||||
private ResourceLoader<IrisMatterObject> matterObjectLoader;
|
||||
private KMap<String, KList<String>> possibleSnippets;
|
||||
private ProceduralObjectCatalog proceduralObjects;
|
||||
private Gson gson;
|
||||
private Gson snippetLoader;
|
||||
private GsonBuilder builder;
|
||||
@@ -383,6 +385,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
this.scriptLoader = registerLoader(IrisScript.class);
|
||||
this.matterObjectLoader = registerLoader(IrisMatterObject.class);
|
||||
this.environment = PackEnvironment.create(this);
|
||||
this.proceduralObjects = new ProceduralObjectCatalog(this);
|
||||
builder.registerTypeAdapterFactory(KeyedType::createTypeAdapter);
|
||||
|
||||
gson = builder.create();
|
||||
@@ -400,6 +403,7 @@ public class IrisData implements ExclusionStrategy, TypeAdapterFactory {
|
||||
for (ResourceLoader<?> i : loaders.values()) {
|
||||
i.clearCache();
|
||||
}
|
||||
proceduralObjects = new ProceduralObjectCatalog(this);
|
||||
}
|
||||
|
||||
public void clearLists() {
|
||||
|
||||
+72
-2
@@ -25,6 +25,8 @@ import com.volmit.iris.engine.mantle.EngineMantle;
|
||||
import com.volmit.iris.engine.mantle.IrisMantleComponent;
|
||||
import com.volmit.iris.engine.mantle.MantleWriter;
|
||||
import com.volmit.iris.engine.object.*;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralObjectSettings;
|
||||
import com.volmit.iris.engine.object.procedural.ResolvedProceduralPlacement;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.collection.KMap;
|
||||
import com.volmit.iris.util.collection.KSet;
|
||||
@@ -42,9 +44,11 @@ import com.volmit.iris.util.parallel.BurstExecutor;
|
||||
import org.bukkit.util.BlockVector;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@ComponentFlag(ReservedFlag.OBJECT)
|
||||
public class MantleObjectComponent extends IrisMantleComponent {
|
||||
@@ -97,12 +101,45 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
placeProceduralObjects(writer, rng, x, z, biome.getProceduralObjects());
|
||||
placeProceduralObjects(writer, rng, x, z, region.getProceduralObjects());
|
||||
}
|
||||
|
||||
@ChunkCoordinates
|
||||
private void placeProceduralObjects(MantleWriter writer, RNG rng, int x, int z, ProceduralObjectSettings settings) {
|
||||
for (ResolvedProceduralPlacement procedural : surfaceProceduralObjects(settings)) {
|
||||
IrisObjectPlacement placement = procedural.placement();
|
||||
if (rng.chance(placement.getChance() + rng.d(-0.005, 0.005))) {
|
||||
try {
|
||||
placeObject(writer, rng, x << 4, z << 4, placement, () -> procedural.variant(rng));
|
||||
} catch (Throwable e) {
|
||||
Iris.reportError(e);
|
||||
Iris.error("Failed to place the procedural object " + procedural.name() + " (" + e.getClass().getSimpleName() + ").");
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<ResolvedProceduralPlacement> surfaceProceduralObjects(ProceduralObjectSettings settings) {
|
||||
if (settings == null) {
|
||||
return List.of();
|
||||
}
|
||||
return settings.resolved(getData()).stream()
|
||||
.filter(procedural -> procedural.placeable() && procedural.placement().getCarvingSupport().supportsSurface())
|
||||
.toList();
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
private void placeObject(MantleWriter writer, RNG rng, int x, int z, IrisObjectPlacement objectPlacement) {
|
||||
placeObject(writer, rng, x, z, objectPlacement, () -> objectPlacement.getObject(getComplex(), rng));
|
||||
}
|
||||
|
||||
@BlockCoordinates
|
||||
private void placeObject(MantleWriter writer, RNG rng, int x, int z, IrisObjectPlacement objectPlacement, Supplier<IrisObject> nextObject) {
|
||||
for (int i = 0; i < objectPlacement.getDensity(rng, x, z, getData()); i++) {
|
||||
IrisObject v = objectPlacement.getScale().get(rng, objectPlacement.getObject(getComplex(), rng));
|
||||
IrisObject v = objectPlacement.getScale().get(rng, nextObject.get());
|
||||
if (v == null) {
|
||||
return;
|
||||
}
|
||||
@@ -151,9 +188,20 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
}
|
||||
|
||||
guessProceduralKeys(v, rng, biome.getProceduralObjects());
|
||||
guessProceduralKeys(v, rng, region.getProceduralObjects());
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
private void guessProceduralKeys(Set<String> keys, RNG rng, ProceduralObjectSettings settings) {
|
||||
for (ResolvedProceduralPlacement procedural : surfaceProceduralObjects(settings)) {
|
||||
if (rng.chance(procedural.placement().getChance() + rng.d(-0.005, 0.005))) {
|
||||
keys.addAll(procedural.variantKeys());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected int computeRadius() {
|
||||
var dimension = getDimension();
|
||||
|
||||
@@ -259,6 +307,28 @@ public class MantleObjectComponent extends IrisMantleComponent {
|
||||
}
|
||||
|
||||
e.complete();
|
||||
return Math.max(xg.get(), zg.get());
|
||||
return Math.max(Math.max(xg.get(), zg.get()), widestProceduralObject(dimension));
|
||||
}
|
||||
|
||||
// Baking every variant here also keeps the first chunk that places one from waiting for it.
|
||||
private int widestProceduralObject(IrisDimension dimension) {
|
||||
int widest = 0;
|
||||
for (var region : dimension.getAllRegions(this::getData)) {
|
||||
widest = Math.max(widest, widestVariant(region.getProceduralObjects()));
|
||||
}
|
||||
for (var biome : dimension.getAllBiomes(this::getData)) {
|
||||
widest = Math.max(widest, widestVariant(biome.getProceduralObjects()));
|
||||
}
|
||||
return widest;
|
||||
}
|
||||
|
||||
private int widestVariant(ProceduralObjectSettings settings) {
|
||||
int widest = 0;
|
||||
for (ResolvedProceduralPlacement procedural : surfaceProceduralObjects(settings)) {
|
||||
for (IrisObject variant : procedural.variants()) {
|
||||
widest = Math.max(widest, Math.max(variant.getW(), variant.getD()));
|
||||
}
|
||||
}
|
||||
return widest;
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import com.volmit.iris.engine.IrisComplex;
|
||||
import com.volmit.iris.engine.data.cache.AtomicCache;
|
||||
import com.volmit.iris.engine.framework.Engine;
|
||||
import com.volmit.iris.engine.object.annotations.*;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralObjectSettings;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.collection.KMap;
|
||||
import com.volmit.iris.util.collection.KSet;
|
||||
@@ -156,6 +157,8 @@ public class IrisBiome extends IrisRegistrant implements IRare {
|
||||
@ArrayType(min = 1, type = IrisObjectPlacement.class)
|
||||
@Desc("Objects define what schematics (iob files) iris will place in this biome")
|
||||
private KList<IrisObjectPlacement> objects = new KList<>();
|
||||
@Desc("Objects Iris generates from scratch instead of loading iob files: trees, ruins, rock formations, coral, fungi and crystals. Only surface placements generate in the world; all of them show in the map's Objects view.")
|
||||
private ProceduralObjectSettings proceduralObjects = null;
|
||||
@Required
|
||||
@ArrayType(min = 1, type = IrisBiomeGeneratorLink.class)
|
||||
@Desc("Generators for this biome. Multiple generators with different interpolation sizes will mix with other biomes how you would expect. This defines your biome height relative to the fluid height. Use negative for oceans.")
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.core.loader.IrisRegistrant;
|
||||
import com.volmit.iris.engine.data.cache.AtomicCache;
|
||||
import com.volmit.iris.engine.object.annotations.*;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralObjectSettings;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.collection.KMap;
|
||||
import com.volmit.iris.util.collection.KSet;
|
||||
@@ -92,6 +93,8 @@ public class IrisRegion extends IrisRegistrant implements IRare {
|
||||
@ArrayType(min = 1, type = IrisObjectPlacement.class)
|
||||
@Desc("Objects define what schematics (iob files) iris will place in this region")
|
||||
private KList<IrisObjectPlacement> objects = new KList<>();
|
||||
@Desc("Objects Iris generates from scratch instead of loading iob files: trees, ruins, rock formations, coral, fungi and crystals. Only surface placements generate in the world; all of them show in the map's Objects view.")
|
||||
private ProceduralObjectSettings proceduralObjects = null;
|
||||
@MinNumber(0)
|
||||
@Desc("The min shore height")
|
||||
private double shoreHeightMin = 1.2;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.volmit.iris.engine.object.procedural;
|
||||
|
||||
public record BlockPosition(int x, int y, int z) {
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural;
|
||||
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
import com.volmit.iris.util.math.Vector3i;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import com.volmit.iris.engine.object.IrisMaterialPalette;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Shared low-level helpers for building procedural objects in memory: resolving a single block id or a noise palette
|
||||
* into a block state, and assembling a raw block map into a centered object shape anchored so its y=0 layer sits one
|
||||
* block above the terrain surface (same convention the tree generator uses).
|
||||
*/
|
||||
public final class IrisProceduralBlocks {
|
||||
public static final String[] FACE_PROPERTIES = {"north", "east", "south", "west", "up", "down"};
|
||||
|
||||
private IrisProceduralBlocks() {
|
||||
}
|
||||
|
||||
public static int[] faceOffset(String face) {
|
||||
return switch (face) {
|
||||
case "north" -> new int[]{0, 0, -1};
|
||||
case "south" -> new int[]{0, 0, 1};
|
||||
case "east" -> new int[]{1, 0, 0};
|
||||
case "west" -> new int[]{-1, 0, 0};
|
||||
case "up" -> new int[]{0, 1, 0};
|
||||
default -> new int[]{0, -1, 0};
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean paletteSet(IrisMaterialPalette palette) {
|
||||
return palette != null && !palette.getPalette().isEmpty();
|
||||
}
|
||||
|
||||
public static boolean paletteSet(Optional<IrisMaterialPalette> palette) {
|
||||
return palette.filter(IrisProceduralBlocks::paletteSet).isPresent();
|
||||
}
|
||||
|
||||
public static BlockData resolve(String block, Optional<IrisMaterialPalette> palette, ObjectBuilder blocks, int x, int y, int z, RNG paletteRng) {
|
||||
return resolve(block, palette.orElse(null), blocks, x, y, z, paletteRng);
|
||||
}
|
||||
|
||||
public static BlockData resolve(Optional<String> block, Optional<IrisMaterialPalette> palette, ObjectBuilder blocks, int x, int y, int z, RNG paletteRng) {
|
||||
return resolve(block.orElse(null), palette.orElse(null), blocks, x, y, z, paletteRng);
|
||||
}
|
||||
|
||||
/** The block of {@code palette} at the position, picked with the seed of {@code paletteRng} as the palette's old class did; else {@code block}. */
|
||||
public static BlockData resolve(String block, IrisMaterialPalette palette, ObjectBuilder blocks, int x, int y, int z, RNG paletteRng) {
|
||||
if (paletteSet(palette)) {
|
||||
return blocks.pick(palette, paletteRng.getSeed(), x, y, z);
|
||||
}
|
||||
if (block != null && !block.isEmpty()) {
|
||||
return blocks.block(block);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean hasProperty(BlockData state, String property) {
|
||||
return propertyValue(state, property) != null;
|
||||
}
|
||||
|
||||
public static String materialKey(BlockData state) {
|
||||
return state.getMaterial().getKey().toString();
|
||||
}
|
||||
|
||||
public static String propertyValue(BlockData state, String property) {
|
||||
String text = state.getAsString();
|
||||
int open = text.indexOf('[');
|
||||
if (open < 0) {
|
||||
return null;
|
||||
}
|
||||
for (String pair : text.substring(open + 1, text.length() - 1).split(",")) {
|
||||
int equals = pair.indexOf('=');
|
||||
if (equals > 0 && pair.substring(0, equals).equals(property)) {
|
||||
return pair.substring(equals + 1);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** The blocks centered on x and z, with their lowest layer one block above the anchor; null when there are none. */
|
||||
public static ProceduralShape assemble(Map<Vector3i, BlockData> blocks) {
|
||||
if (blocks == null || blocks.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int minX = Integer.MAX_VALUE;
|
||||
int minY = Integer.MAX_VALUE;
|
||||
int minZ = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE;
|
||||
int maxY = Integer.MIN_VALUE;
|
||||
int maxZ = Integer.MIN_VALUE;
|
||||
for (Vector3i v : blocks.keySet()) {
|
||||
minX = Math.min(minX, v.getBlockX());
|
||||
minY = Math.min(minY, v.getBlockY());
|
||||
minZ = Math.min(minZ, v.getBlockZ());
|
||||
maxX = Math.max(maxX, v.getBlockX());
|
||||
maxY = Math.max(maxY, v.getBlockY());
|
||||
maxZ = Math.max(maxZ, v.getBlockZ());
|
||||
}
|
||||
|
||||
int w = maxX - minX + 1;
|
||||
int h = maxY - minY + 1;
|
||||
int d = maxZ - minZ + 1;
|
||||
int cx = w / 2;
|
||||
int cy = h / 2;
|
||||
int cz = d / 2;
|
||||
|
||||
List<ObjectBlock> placed = new ArrayList<>(blocks.size());
|
||||
for (Map.Entry<Vector3i, BlockData> entry : blocks.entrySet()) {
|
||||
Vector3i v = entry.getKey();
|
||||
int nx = v.getBlockX() - minX - cx;
|
||||
int ny = v.getBlockY() - cy + 1;
|
||||
int nz = v.getBlockZ() - minZ - cz;
|
||||
placed.add(new ObjectBlock(new BlockPosition(nx, ny, nz), entry.getValue()));
|
||||
}
|
||||
|
||||
return new ProceduralShape(w, h, d, placed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.volmit.iris.engine.object.procedural;
|
||||
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
public record ObjectBlock(BlockPosition position, BlockData state) {
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.volmit.iris.engine.object.procedural;
|
||||
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.engine.object.IrisMaterialPalette;
|
||||
import com.volmit.iris.engine.object.IrisObject;
|
||||
import com.volmit.iris.util.collection.KMap;
|
||||
import com.volmit.iris.util.data.B;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
import com.volmit.iris.util.math.Vector3i;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Turns the blocks a procedural generator asks for into the pack's block data, and its shapes into objects. One builder
|
||||
* serves one bake on one thread.
|
||||
*/
|
||||
public final class ObjectBuilder {
|
||||
private final IrisData data;
|
||||
private final KMap<String, BlockData> withPropertyCache = new KMap<>();
|
||||
|
||||
public ObjectBuilder(IrisData data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/** The block {@code key} names; null when there is no such block. */
|
||||
public BlockData block(String key) {
|
||||
return B.getOrNull(key, false);
|
||||
}
|
||||
|
||||
/** The block {@code palette} gives at the position when its noise is seeded with {@code seed}. */
|
||||
public BlockData pick(IrisMaterialPalette palette, long seed, int x, int y, int z) {
|
||||
return palette.get(new RNG(seed), x, y, z, data);
|
||||
}
|
||||
|
||||
/** {@code state} with the property set; {@code state} itself when the block has no such property or value. */
|
||||
public BlockData withProperty(BlockData state, String name, String value) {
|
||||
String key = state.getAsString() + "|" + name + "=" + value;
|
||||
return withPropertyCache.computeIfAbsent(key, ignored -> merged(state, name, value)).clone();
|
||||
}
|
||||
|
||||
private static BlockData merged(BlockData state, String name, String value) {
|
||||
try {
|
||||
return state.merge(Bukkit.createBlockData(state.getMaterial(), "[" + name + "=" + value + "]"));
|
||||
} catch (IllegalArgumentException unknownProperty) {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
/** The object of {@code blocks}, {@code width} by {@code height} by {@code depth}, loaded under {@code loadKey}. */
|
||||
public IrisObject build(String loadKey, int width, int height, int depth, List<ObjectBlock> blocks) {
|
||||
IrisObject object = new IrisObject(width, height, depth);
|
||||
for (ObjectBlock block : blocks) {
|
||||
BlockPosition position = block.position();
|
||||
object.getBlocks().put(new Vector3i(position.x(), position.y(), position.z()), block.state());
|
||||
}
|
||||
object.setLoadKey(loadKey);
|
||||
object.setLoader(data);
|
||||
return object;
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.volmit.iris.engine.object.procedural;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.engine.object.IrisObject;
|
||||
import com.volmit.iris.engine.object.procedural.coral.ResolvedCoral;
|
||||
import com.volmit.iris.engine.object.procedural.crystal.ResolvedCrystal;
|
||||
import com.volmit.iris.engine.object.procedural.formation.ResolvedFormation;
|
||||
import com.volmit.iris.engine.object.procedural.fungi.ResolvedFungus;
|
||||
import com.volmit.iris.engine.object.procedural.ruin.ResolvedRuin;
|
||||
import com.volmit.iris.engine.object.procedural.tree.ResolvedTree;
|
||||
import com.volmit.iris.util.collection.KMap;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* The procedural objects of one pack. A definition several biomes share, such as one snippet, is baked once, and every
|
||||
* variant can be found again by its key.
|
||||
*/
|
||||
public final class ProceduralObjectCatalog {
|
||||
private static final Gson DEFINITION_JSON = new Gson();
|
||||
|
||||
private final IrisData data;
|
||||
private final KMap<String, ResolvedProceduralPlacement> byDefinition = new KMap<>();
|
||||
private final KMap<String, ResolvedProceduralPlacement> byVariantKey = new KMap<>();
|
||||
|
||||
public ProceduralObjectCatalog(IrisData data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/** The placements {@code settings} declare, shared with every other declaration of the same definitions. */
|
||||
public List<ResolvedProceduralPlacement> resolve(ProceduralObjectSettings settings) {
|
||||
List<ResolvedProceduralPlacement> all = new ArrayList<>();
|
||||
addAll(all, settings.trees(), tree -> new ResolvedTree(tree, data));
|
||||
addAll(all, settings.ruins(), ruin -> new ResolvedRuin(ruin, data));
|
||||
addAll(all, settings.formations(), formation -> new ResolvedFormation(formation, data));
|
||||
addAll(all, settings.coral(), coral -> new ResolvedCoral(coral, data));
|
||||
addAll(all, settings.fungi(), fungus -> new ResolvedFungus(fungus, data));
|
||||
addAll(all, settings.crystals(), crystal -> new ResolvedCrystal(crystal, data));
|
||||
return List.copyOf(all);
|
||||
}
|
||||
|
||||
private <S> void addAll(List<ResolvedProceduralPlacement> all, List<S> definitions, Function<S, ResolvedProceduralPlacement> resolve) {
|
||||
for (S definition : definitions) {
|
||||
String identity = definition.getClass().getName() + DEFINITION_JSON.toJson(definition);
|
||||
all.add(byDefinition.computeIfAbsent(identity, ignored -> register(resolve.apply(definition))));
|
||||
}
|
||||
}
|
||||
|
||||
private ResolvedProceduralPlacement register(ResolvedProceduralPlacement placement) {
|
||||
for (String key : placement.variantKeys()) {
|
||||
byVariantKey.putIfAbsent(key, placement);
|
||||
}
|
||||
return placement;
|
||||
}
|
||||
|
||||
/** The key of every variant the pack's regions and biomes declare, whether or not its dimensions reach them. */
|
||||
public List<String> variantKeys() {
|
||||
Set<String> keys = new LinkedHashSet<>();
|
||||
data.getRegionLoader().streamAll().forEach(region -> addVariantKeys(keys, region.getProceduralObjects()));
|
||||
data.getBiomeLoader().streamAll().forEach(biome -> addVariantKeys(keys, biome.getProceduralObjects()));
|
||||
return List.copyOf(keys);
|
||||
}
|
||||
|
||||
private void addVariantKeys(Set<String> keys, ProceduralObjectSettings settings) {
|
||||
if (settings == null) {
|
||||
return;
|
||||
}
|
||||
for (ResolvedProceduralPlacement placement : settings.resolved(data)) {
|
||||
keys.addAll(placement.variantKeys());
|
||||
}
|
||||
}
|
||||
|
||||
/** The baked variant loaded under {@code key}; null when no definition resolved so far has it, or it baked no blocks. */
|
||||
public IrisObject variant(String key) {
|
||||
ResolvedProceduralPlacement placement = byVariantKey.get(key);
|
||||
return placement == null ? null : placement.variant(key);
|
||||
}
|
||||
|
||||
public static boolean isVariantKey(String key) {
|
||||
return key.startsWith("procedural/");
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.coral.Coral;
|
||||
import com.volmit.iris.engine.object.procedural.crystal.Crystal;
|
||||
import com.volmit.iris.engine.object.procedural.formation.Formation;
|
||||
import com.volmit.iris.engine.object.procedural.fungi.Fungus;
|
||||
import com.volmit.iris.engine.object.procedural.ruin.Ruin;
|
||||
import com.volmit.iris.engine.object.procedural.tree.Tree;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.engine.data.cache.AtomicCache;
|
||||
import com.volmit.iris.engine.object.annotations.ArrayType;
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
import com.volmit.iris.engine.object.annotations.Snippet;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The procedural objects a region or biome declares, written as {@code "proceduralObjects": {"trees": [...], ...}}: things
|
||||
* Iris generates from scratch at world-gen time and places like object placements.
|
||||
*/
|
||||
@Snippet("procedural-objects")
|
||||
@Desc("Procedurally generated objects placed in a biome or region. Unlike the objects block (which loads iob files), these are generated from scratch at world-gen time.")
|
||||
public final class ProceduralObjectSettings {
|
||||
private final transient AtomicCache<List<ResolvedProceduralPlacement>> resolved = new AtomicCache<>();
|
||||
|
||||
@ArrayType(min = 1, type = Tree.class)
|
||||
@Desc("Procedurally generated trees.")
|
||||
private List<Tree> trees = List.of();
|
||||
|
||||
@ArrayType(min = 1, type = Ruin.class)
|
||||
@Desc("Procedurally generated ruins (crumbling pillars, walls, arches, rubble).")
|
||||
private List<Ruin> ruins = List.of();
|
||||
|
||||
@ArrayType(min = 1, type = Formation.class)
|
||||
@Desc("Procedurally generated natural rock formations (spires, hoodoos, arches, sea stacks, boulders, basalt columns).")
|
||||
private List<Formation> formations = List.of();
|
||||
|
||||
@ArrayType(min = 1, type = Coral.class)
|
||||
@Desc("Procedurally generated underwater coral structures.")
|
||||
private List<Coral> coral = List.of();
|
||||
|
||||
@ArrayType(min = 1, type = Fungus.class)
|
||||
@Desc("Procedurally generated mushrooms and shelf fungi.")
|
||||
private List<Fungus> fungi = List.of();
|
||||
|
||||
@ArrayType(min = 1, type = Crystal.class)
|
||||
@Desc("Procedurally generated crystal clusters (usually placed in caves).")
|
||||
private List<Crystal> crystals = List.of();
|
||||
|
||||
public ProceduralObjectSettings() {
|
||||
}
|
||||
|
||||
/** Every procedural object declared here, made placeable through the pack's catalog. */
|
||||
public List<ResolvedProceduralPlacement> resolved(IrisData data) {
|
||||
return resolved.aquire(() -> data.getProceduralObjects().resolve(this));
|
||||
}
|
||||
|
||||
public List<Tree> trees() {
|
||||
return view(trees);
|
||||
}
|
||||
|
||||
public List<Ruin> ruins() {
|
||||
return view(ruins);
|
||||
}
|
||||
|
||||
public List<Formation> formations() {
|
||||
return view(formations);
|
||||
}
|
||||
|
||||
public List<Coral> coral() {
|
||||
return view(coral);
|
||||
}
|
||||
|
||||
public List<Fungus> fungi() {
|
||||
return view(fungi);
|
||||
}
|
||||
|
||||
public List<Crystal> crystals() {
|
||||
return view(crystals);
|
||||
}
|
||||
|
||||
private static <T> List<T> view(List<T> list) {
|
||||
return list == null ? List.of() : Collections.unmodifiableList(list);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** A procedural object's blocks around its anchor, in the order they were made, and the size of the box they fill. */
|
||||
public record ProceduralShape(int width, int height, int depth, List<ObjectBlock> blocks) {
|
||||
}
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package com.volmit.iris.engine.object.procedural;
|
||||
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.engine.data.cache.AtomicCache;
|
||||
import com.volmit.iris.engine.object.CarvingMode;
|
||||
import com.volmit.iris.engine.object.IrisObject;
|
||||
import com.volmit.iris.engine.object.IrisObjectLimit;
|
||||
import com.volmit.iris.engine.object.IrisObjectPlacement;
|
||||
import com.volmit.iris.engine.object.IrisObjectRotation;
|
||||
import com.volmit.iris.engine.object.IrisObjectTranslate;
|
||||
import com.volmit.iris.engine.object.IrisStiltSettings;
|
||||
import com.volmit.iris.engine.object.ObjectPlaceMode;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A procedural object of a region or biome made placeable: a pool of variants baked once from its settings, and the
|
||||
* object placement they are scattered with.
|
||||
*/
|
||||
public abstract class ResolvedProceduralPlacement {
|
||||
private static final long VARIANT_SEED_STRIDE = 7919L;
|
||||
|
||||
/** The options every procedural placement shares with an object placement. */
|
||||
public record Shape(String kind, String name, double chance, int density, long seed, int variants, ObjectPlaceMode mode,
|
||||
IrisObjectRotation rotation, IrisObjectLimit clamp, CarvingMode carvingSupport, boolean underwater,
|
||||
IrisObjectTranslate translate, IrisStiltSettings stiltSettings) {
|
||||
}
|
||||
|
||||
private final Shape shape;
|
||||
private final IrisData data;
|
||||
private final AtomicCache<List<IrisObject>> variantCache = new AtomicCache<>();
|
||||
private final AtomicCache<IrisObjectPlacement> placementCache = new AtomicCache<>();
|
||||
|
||||
protected ResolvedProceduralPlacement(Shape shape, IrisData data) {
|
||||
this.shape = shape;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/** The shape variant {@code index} is baked as, from its own seeded stream; null when it has no blocks. */
|
||||
protected abstract ProceduralShape generate(int index, RNG rng, ObjectBuilder blocks);
|
||||
|
||||
/** The number of variants to bake; the settings' count, at least one. */
|
||||
protected int variantCount() {
|
||||
return Math.max(1, shape.variants());
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return shape.name();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the world generator can place it. A place mode this version does not know, such as v4's CEILING_HANG,
|
||||
* reads as null, and placing such an object on the surface would put it in the wrong place.
|
||||
*/
|
||||
public boolean placeable() {
|
||||
return shape.mode() != null;
|
||||
}
|
||||
|
||||
/** The object placement the variants are placed with. */
|
||||
public IrisObjectPlacement placement() {
|
||||
return placementCache.aquire(this::createPlacement);
|
||||
}
|
||||
|
||||
private IrisObjectPlacement createPlacement() {
|
||||
IrisObjectPlacement placement = new IrisObjectPlacement();
|
||||
placement.setPlace(new KList<>(variantKeys()));
|
||||
placement.setChance(shape.chance());
|
||||
placement.setDensity(Math.max(1, shape.density()));
|
||||
placement.setMode(shape.mode() == null ? ObjectPlaceMode.CENTER_HEIGHT : shape.mode());
|
||||
placement.setCarvingSupport(shape.carvingSupport() == null ? CarvingMode.SURFACE_ONLY : shape.carvingSupport());
|
||||
placement.setUnderwater(shape.underwater());
|
||||
placement.setStiltSettings(shape.stiltSettings());
|
||||
if (shape.rotation() != null) {
|
||||
placement.setRotation(shape.rotation());
|
||||
}
|
||||
if (shape.clamp() != null) {
|
||||
placement.setClamp(shape.clamp());
|
||||
}
|
||||
if (shape.translate() != null) {
|
||||
placement.setTranslate(shape.translate());
|
||||
}
|
||||
return placement;
|
||||
}
|
||||
|
||||
/** The key of every variant the settings ask for, whether or not it baked any blocks. */
|
||||
public List<String> variantKeys() {
|
||||
List<String> keys = new ArrayList<>();
|
||||
for (int index = 0; index < variantCount(); index++) {
|
||||
keys.add(variantKey(index));
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
/** The key variant {@code index} is loaded under, like {@code procedural/tree/oak#0}. */
|
||||
public String variantKey(int index) {
|
||||
return "procedural/" + shape.kind() + "/" + shape.name() + "#" + index;
|
||||
}
|
||||
|
||||
/** Every variant, baked on first use. */
|
||||
public List<IrisObject> variants() {
|
||||
return variantCache.aquire(() -> bake(new ObjectBuilder(data)));
|
||||
}
|
||||
|
||||
/** One of the variants, picked with {@code rng}; null when none baked. */
|
||||
public IrisObject variant(RNG rng) {
|
||||
List<IrisObject> baked = variants();
|
||||
if (baked.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return baked.get(rng.i(baked.size()));
|
||||
}
|
||||
|
||||
/** The baked variant loaded under {@code key}; null when it is not one of these, or baked no blocks. */
|
||||
public IrisObject variant(String key) {
|
||||
for (IrisObject variant : variants()) {
|
||||
if (variant.getLoadKey().equals(key)) {
|
||||
return variant;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Bakes every variant; the default draws variant {@code i} from {@code seed + i * stride}. */
|
||||
protected List<IrisObject> bake(ObjectBuilder blocks) {
|
||||
List<IrisObject> baked = new ArrayList<>();
|
||||
int count = variantCount();
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
ProceduralShape generated = generate(i, variantRng(i), blocks);
|
||||
if (generated == null || generated.blocks().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
baked.add(build(blocks, variantKey(i), generated));
|
||||
}
|
||||
|
||||
return baked;
|
||||
}
|
||||
|
||||
protected static IrisObject build(ObjectBuilder blocks, String loadKey, ProceduralShape generated) {
|
||||
return blocks.build(loadKey, generated.width(), generated.height(), generated.depth(), generated.blocks());
|
||||
}
|
||||
|
||||
protected final RNG variantRng(int index) {
|
||||
return new RNG(shape.seed() + (index * VARIANT_SEED_STRIDE));
|
||||
}
|
||||
|
||||
protected final long seed() {
|
||||
return shape.seed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,336 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.coral;
|
||||
|
||||
import com.volmit.iris.engine.object.ObjectPlaceMode;
|
||||
import com.volmit.iris.engine.object.CarvingMode;
|
||||
import com.volmit.iris.engine.object.IrisObjectRotation;
|
||||
import com.volmit.iris.engine.object.IrisObjectTranslate;
|
||||
import com.volmit.iris.engine.object.IrisStiltSettings;
|
||||
import com.volmit.iris.engine.object.IrisObjectLimit;
|
||||
import com.volmit.iris.engine.object.IrisMaterialPalette;
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
import com.volmit.iris.engine.object.annotations.Snippet;
|
||||
import com.volmit.iris.engine.object.annotations.MaxNumber;
|
||||
import com.volmit.iris.engine.object.annotations.MinNumber;
|
||||
import com.volmit.iris.engine.object.annotations.RegistryListBlockType;
|
||||
import com.volmit.iris.engine.object.annotations.Required;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Snippet("coral")
|
||||
@Desc("A single procedurally generated coral reef structure. Iris bakes a pool of deterministic variants from these settings and scatters them across the seafloor at world-gen time, exactly like an object placement but generated from scratch instead of loaded from an iob file. Mirrors the procedural tree system but tuned for underwater reefs: structural blocks are waterlogged so the coral stays alive, and placement defaults to anchoring on the terrain beneath the water surface.")
|
||||
public final class Coral {
|
||||
@Desc("A human readable name used in logs and as the variant load key.")
|
||||
private String name = "coral";
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The chance per chunk for this coral to attempt placement. Use density for multiple per chunk.")
|
||||
private double chance = 0.4;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("If the chance check passes, attempt this many placements in the chunk.")
|
||||
private int density = 1;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("How many distinct variants to pre-bake for this coral. Higher means more variety at a small memory cost.")
|
||||
private int variants = 6;
|
||||
|
||||
@Desc("The base seed for deterministic generation. The same seed and settings always bake the same variants.")
|
||||
private long seed = 1337;
|
||||
|
||||
@Desc("The placement mode used to anchor the coral to the seafloor.")
|
||||
private ObjectPlaceMode mode = ObjectPlaceMode.CENTER_HEIGHT;
|
||||
|
||||
@Desc("Rotate this coral's placement.")
|
||||
private IrisObjectRotation rotation = new IrisObjectRotation();
|
||||
|
||||
@Desc("Limit the max or min height of placement.")
|
||||
private IrisObjectLimit clamp = new IrisObjectLimit();
|
||||
|
||||
@Desc("Whether this coral may place on the terrain surface, under carvings, or both.")
|
||||
private CarvingMode carvingSupport = CarvingMode.SURFACE_ONLY;
|
||||
|
||||
@Desc("If true (default for coral), the coral anchors on the terrain height beneath the water, so it grows up from the seafloor rather than floating at the water surface.")
|
||||
private boolean underwater = true;
|
||||
|
||||
@Desc("Translate (offset) this placement along each axis; for example set a negative y to sink it into the seafloor.")
|
||||
private IrisObjectTranslate translate = new IrisObjectTranslate();
|
||||
|
||||
@Desc("Settings for the stilt place modes (STILT, MIN_STILT, FAST_STILT, CENTER_STILT, ERODE_STILT, ORGANIC_STILT).")
|
||||
private IrisStiltSettings stiltSettings;
|
||||
|
||||
@Desc("If true (default), every resolved coral block that is waterloggable is forced waterlogged so the coral does not die in water (coral fans, sea pickles and kelp also waterlog). Set false to place dead/dry coral.")
|
||||
private boolean waterlogged = true;
|
||||
|
||||
@Desc("The overall silhouette this coral grows into (branching, fan, brain, pillar, tendril). Each form runs a different generation routine.")
|
||||
private CoralForm form = CoralForm.BRANCHING;
|
||||
|
||||
@Required
|
||||
@Desc("The structural coral block (the living wood of the reef), e.g. minecraft:tube_coral_block. Ignored when blockPalette is set.")
|
||||
@RegistryListBlockType
|
||||
private String block = "minecraft:tube_coral_block";
|
||||
|
||||
@Desc("A noise-driven palette for the structural coral block. When set this overrides the single block, letting the reef mix tube/brain/bubble/fire/horn coral_block tones across its body.")
|
||||
private IrisMaterialPalette blockPalette;
|
||||
|
||||
@Desc("Optional tip block placed at branch tips and at the very top of the structure, e.g. coral fans or minecraft:sea_pickle. Ignored when tipPalette is set. Null disables tips.")
|
||||
@RegistryListBlockType
|
||||
private String tipBlock = null;
|
||||
|
||||
@Desc("A noise-driven palette for the tip block. When set this overrides the single tipBlock, letting tips mix fan / pickle decorations.")
|
||||
private IrisMaterialPalette tipPalette;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The chance per eligible tip position to place a tip block (coral fan / sea pickle).")
|
||||
private double tipChance = 0.6;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Minimum overall height of the coral in blocks.")
|
||||
private int heightMin = 4;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Maximum overall height of the coral in blocks.")
|
||||
private int heightMax = 8;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Horizontal spread of the structure in blocks. For BRANCHING this is how far arms reach out, for BRAIN/PILLAR/FAN it widens the base footprint.")
|
||||
private double spread = 3;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Lateral noise wobble (0-1) applied to branches and tendrils so they are not ruler-straight. 0 is perfectly straight, 1 is heavily wandering.")
|
||||
private double sway = 0.5;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(12)
|
||||
@Desc("BRANCHING only: how many arms sprout from the central stalk.")
|
||||
private int branchCount = 4;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("BRANCHING only: the length of each arm in blocks before reaching its tip.")
|
||||
private double branchLength = 3;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(90)
|
||||
@Desc("BRANCHING only: the upward elevation in degrees of each arm from horizontal. 90 is straight up, 0 is flat out.")
|
||||
private double branchElevation = 55;
|
||||
|
||||
@Desc("BRANCHING only: how the arms are distributed around the stalk by azimuth.")
|
||||
private CoralBranchAzimuth branchAzimuth = CoralBranchAzimuth.GOLDEN_ANGLE;
|
||||
|
||||
@Desc("BRANCHING only: if true, each arm splits once into a small fan of sub-arms for a bushier reef.")
|
||||
private boolean subBranches = true;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(5)
|
||||
@Desc("BRANCHING only: how many sub-arms each arm splits into when subBranches is true.")
|
||||
private int subBranchCount = 2;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("BRANCHING only: the length of each sub-arm as a fraction of its parent arm.")
|
||||
private double subBranchScale = 0.5;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(4)
|
||||
@Desc("BRANCHING and PILLAR only: the radius of the small coral tip cluster grown at each tip.")
|
||||
private int tipClusterRadius = 1;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(8)
|
||||
@Desc("BRAIN only: the horizontal radius of the brain-coral blob in blocks.")
|
||||
private int brainRadius = 3;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("BRAIN only: how strongly 3D value noise perturbs the ellipsoid surface, for a wrinkled organic blob. 0 is a smooth dome.")
|
||||
private double brainRoughness = 0.35;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(12)
|
||||
@Desc("PILLAR only: the radius of the stout vertical column in blocks.")
|
||||
private int pillarRadius = 1;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(8)
|
||||
@Desc("FAN only: the half-width of the upright fan plane in blocks (the disc widens toward its mid-height).")
|
||||
private int fanWidth = 3;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(12)
|
||||
@Desc("TENDRIL only: how many thin wavy stalks rise from the base.")
|
||||
private int tendrilCount = 4;
|
||||
|
||||
public Coral() {
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public double chance() {
|
||||
return chance;
|
||||
}
|
||||
|
||||
public int density() {
|
||||
return density;
|
||||
}
|
||||
|
||||
public int variants() {
|
||||
return variants;
|
||||
}
|
||||
|
||||
public long seed() {
|
||||
return seed;
|
||||
}
|
||||
|
||||
public ObjectPlaceMode mode() {
|
||||
return mode == null ? ObjectPlaceMode.CENTER_HEIGHT : mode;
|
||||
}
|
||||
|
||||
public IrisObjectRotation rotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public IrisObjectLimit clamp() {
|
||||
return clamp;
|
||||
}
|
||||
|
||||
public CarvingMode carvingSupport() {
|
||||
return carvingSupport == null ? CarvingMode.SURFACE_ONLY : carvingSupport;
|
||||
}
|
||||
|
||||
public boolean underwater() {
|
||||
return underwater;
|
||||
}
|
||||
|
||||
public IrisObjectTranslate translate() {
|
||||
return translate;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisStiltSettings> stiltSettings() {
|
||||
return Optional.ofNullable(stiltSettings);
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
|
||||
public boolean waterlogged() {
|
||||
return waterlogged;
|
||||
}
|
||||
|
||||
public CoralForm form() {
|
||||
return form == null ? CoralForm.BRANCHING : form;
|
||||
}
|
||||
|
||||
public String block() {
|
||||
return block;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisMaterialPalette> blockPalette() {
|
||||
return Optional.ofNullable(blockPalette);
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<String> tipBlock() {
|
||||
return Optional.ofNullable(tipBlock);
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisMaterialPalette> tipPalette() {
|
||||
return Optional.ofNullable(tipPalette);
|
||||
}
|
||||
|
||||
public double tipChance() {
|
||||
return tipChance;
|
||||
}
|
||||
|
||||
public int heightMin() {
|
||||
return heightMin;
|
||||
}
|
||||
|
||||
public int heightMax() {
|
||||
return heightMax;
|
||||
}
|
||||
|
||||
public double spread() {
|
||||
return spread;
|
||||
}
|
||||
|
||||
public double sway() {
|
||||
return sway;
|
||||
}
|
||||
|
||||
public int branchCount() {
|
||||
return branchCount;
|
||||
}
|
||||
|
||||
public double branchLength() {
|
||||
return branchLength;
|
||||
}
|
||||
|
||||
public double branchElevation() {
|
||||
return branchElevation;
|
||||
}
|
||||
|
||||
public CoralBranchAzimuth branchAzimuth() {
|
||||
return branchAzimuth == null ? CoralBranchAzimuth.GOLDEN_ANGLE : branchAzimuth;
|
||||
}
|
||||
|
||||
public boolean subBranches() {
|
||||
return subBranches;
|
||||
}
|
||||
|
||||
public int subBranchCount() {
|
||||
return subBranchCount;
|
||||
}
|
||||
|
||||
public double subBranchScale() {
|
||||
return subBranchScale;
|
||||
}
|
||||
|
||||
public int tipClusterRadius() {
|
||||
return tipClusterRadius;
|
||||
}
|
||||
|
||||
public int brainRadius() {
|
||||
return brainRadius;
|
||||
}
|
||||
|
||||
public double brainRoughness() {
|
||||
return brainRoughness;
|
||||
}
|
||||
|
||||
public int pillarRadius() {
|
||||
return pillarRadius;
|
||||
}
|
||||
|
||||
public int fanWidth() {
|
||||
return fanWidth;
|
||||
}
|
||||
|
||||
public int tendrilCount() {
|
||||
return tendrilCount;
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.coral;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("How the arms of a BRANCHING coral are distributed around the central stalk by compass azimuth.")
|
||||
public enum CoralBranchAzimuth {
|
||||
@Desc("Distribute arms by the golden angle (about 137.5 degrees per arm) for a natural phyllotactic spiral where no two arms overlap.")
|
||||
GOLDEN_ANGLE,
|
||||
|
||||
@Desc("Distribute arms in evenly spaced spokes, dividing 360 degrees equally between them like a radial whorl.")
|
||||
EVEN,
|
||||
|
||||
@Desc("Place each arm at a fully random azimuth using the deterministic per-variant RNG, for an irregular wild reef.")
|
||||
RANDOM
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.coral;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class CoralCanvas {
|
||||
public enum Role {
|
||||
STRUCTURE,
|
||||
TIP
|
||||
}
|
||||
|
||||
private static final long BIAS = 1L << 20;
|
||||
private static final long MASK = (1L << 21) - 1L;
|
||||
|
||||
private final Map<Long, Role> cells = new HashMap<>();
|
||||
|
||||
public void set(int x, int y, int z, Role role) {
|
||||
if (y < 0) {
|
||||
return;
|
||||
}
|
||||
long key = encode(x, y, z);
|
||||
Role existing = cells.get(key);
|
||||
if (existing == Role.TIP && role == Role.STRUCTURE) {
|
||||
return;
|
||||
}
|
||||
cells.put(key, role);
|
||||
}
|
||||
|
||||
public Map<Long, Role> getCells() {
|
||||
return cells;
|
||||
}
|
||||
|
||||
public static long encode(int x, int y, int z) {
|
||||
long ex = (x + BIAS) & MASK;
|
||||
long ey = (y + BIAS) & MASK;
|
||||
long ez = (z + BIAS) & MASK;
|
||||
return (ex << 42) | (ey << 21) | ez;
|
||||
}
|
||||
|
||||
public static int[] decode(long key) {
|
||||
int ez = (int) ((key & MASK) - BIAS);
|
||||
int ey = (int) (((key >> 21) & MASK) - BIAS);
|
||||
int ex = (int) (((key >> 42) & MASK) - BIAS);
|
||||
return new int[]{ex, ey, ez};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.coral;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("The overall silhouette a procedural coral grows into. Each form drives a completely different generation routine in the coral generator, from tree-like branching arms to rounded brain blobs and thin wavy tendrils.")
|
||||
public enum CoralForm {
|
||||
@Desc("A tree-like coral built from a stout central stalk that sprouts several upward-leaning arms (distributed by golden angle or randomly), each optionally splitting once into sub-arms, with small coral tip clusters (fans / sea pickles) at the ends. The most structurally complex form and the closest analogue to a procedural tree.")
|
||||
BRANCHING,
|
||||
|
||||
@Desc("A thin, single-block-thick vertical fan plane that rises and widens with height like a sheet of fire or horn coral. Reads as a flat decorative blade rather than a volume.")
|
||||
FAN,
|
||||
|
||||
@Desc("A rounded, noise-perturbed ellipsoid blob of brain coral that bulges out of the seafloor like a boulder, with an irregular wrinkled surface driven by 3D value noise.")
|
||||
BRAIN,
|
||||
|
||||
@Desc("A stout, thick vertical column of coral block topped with a dense tip cluster. Wider and blunter than a tendril, reading as a solid coral pillar or stack.")
|
||||
PILLAR,
|
||||
|
||||
@Desc("Several very thin (one block wide) wavy stalks that rise upward and wander laterally using value noise, like a clump of swaying coral whips or kelp-thin growths.")
|
||||
TENDRIL
|
||||
}
|
||||
+262
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.coral;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBuilder;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralShape;
|
||||
import com.volmit.iris.engine.object.procedural.IrisProceduralBlocks;
|
||||
import com.volmit.iris.engine.object.procedural.tree.TreeFunctions;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import com.volmit.iris.util.math.Vector3i;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class CoralGenerator {
|
||||
private static final double GOLDEN_ANGLE = 137.50776405003785;
|
||||
|
||||
private CoralGenerator() {
|
||||
}
|
||||
|
||||
public static ProceduralShape generate(Coral coral, int variantIndex, RNG rng, ObjectBuilder blocks) {
|
||||
int lo = Math.min(coral.heightMin(), coral.heightMax());
|
||||
int hi = Math.max(coral.heightMin(), coral.heightMax());
|
||||
int height = Math.max(2, rng.i(lo, hi + 1));
|
||||
long shapeSeed = rng.getSeed() + variantIndex * 31L;
|
||||
|
||||
CoralCanvas canvas = new CoralCanvas();
|
||||
switch (coral.form()) {
|
||||
case BRANCHING -> buildBranching(canvas, coral, height, shapeSeed, rng);
|
||||
case FAN -> buildFan(canvas, coral, height, shapeSeed);
|
||||
case BRAIN -> buildBrain(canvas, coral, height, shapeSeed);
|
||||
case PILLAR -> buildPillar(canvas, coral, height, shapeSeed);
|
||||
case TENDRIL -> buildTendril(canvas, coral, height, shapeSeed, rng);
|
||||
}
|
||||
|
||||
Map<Vector3i, BlockData> resolved = resolve(canvas, coral, blocks);
|
||||
return IrisProceduralBlocks.assemble(resolved);
|
||||
}
|
||||
|
||||
private static void buildBranching(CoralCanvas canvas, Coral coral, int height, long seed, RNG rng) {
|
||||
int stalkTop = Math.max(1, (int) Math.round(height * 0.5));
|
||||
for (int y = 0; y < stalkTop; y++) {
|
||||
placeWavy(canvas, coral, 0, y, 0, y, seed, CoralCanvas.Role.STRUCTURE);
|
||||
}
|
||||
|
||||
int count = Math.max(1, coral.branchCount());
|
||||
for (int b = 0; b < count; b++) {
|
||||
double az = branchAzimuth(coral, b, count, rng);
|
||||
int startY = stalkTop - 1 + (int) Math.round((b % 2) * 1.0);
|
||||
double[] tip = rasterizeArm(canvas, coral, 0, startY, 0, az,
|
||||
coral.branchElevation(), coral.branchLength(), seed + b * 131L);
|
||||
|
||||
placeTipCluster(canvas, coral, (int) Math.round(tip[0]), (int) Math.round(tip[1]), (int) Math.round(tip[2]),
|
||||
coral.tipClusterRadius(), seed + b * 977L);
|
||||
|
||||
if (coral.subBranches()) {
|
||||
int subCount = Math.max(1, coral.subBranchCount());
|
||||
for (int s = 0; s < subCount; s++) {
|
||||
double subAz = az + (s - (subCount - 1) / 2.0) * 35.0;
|
||||
double subLen = coral.branchLength() * coral.subBranchScale();
|
||||
double[] subTip = rasterizeArm(canvas, coral,
|
||||
(int) Math.round(tip[0]), (int) Math.round(tip[1]), (int) Math.round(tip[2]),
|
||||
subAz, coral.branchElevation() + 10.0, subLen, seed + b * 131L + s * 17L + 5000L);
|
||||
placeTipCluster(canvas, coral, (int) Math.round(subTip[0]), (int) Math.round(subTip[1]), (int) Math.round(subTip[2]),
|
||||
Math.max(0, coral.tipClusterRadius() - 1), seed + b * 977L + s * 53L);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void buildFan(CoralCanvas canvas, Coral coral, int height, long seed) {
|
||||
int half = Math.max(1, coral.fanWidth());
|
||||
for (int y = 0; y < height; y++) {
|
||||
double t = y / (double) Math.max(1, height - 1);
|
||||
double profile = Math.sin(Math.PI * t);
|
||||
int w = (int) Math.round(half * profile);
|
||||
for (int dx = -w; dx <= w; dx++) {
|
||||
canvas.set(dx, y, 0, CoralCanvas.Role.STRUCTURE);
|
||||
}
|
||||
}
|
||||
for (int dx = -half; dx <= half; dx++) {
|
||||
placeTip(canvas, coral, dx, height - 1, 0, seed + dx);
|
||||
}
|
||||
}
|
||||
|
||||
private static void buildBrain(CoralCanvas canvas, Coral coral, int height, long seed) {
|
||||
int r = Math.max(1, coral.brainRadius());
|
||||
int ry = Math.max(1, Math.min(r, (int) Math.round(height * 0.6)));
|
||||
double rough = coral.brainRoughness();
|
||||
for (int dx = -r; dx <= r; dx++) {
|
||||
for (int dz = -r; dz <= r; dz++) {
|
||||
for (int dy = 0; dy <= ry * 2; dy++) {
|
||||
double nx = dx / (double) r;
|
||||
double ny = (dy - ry) / (double) ry;
|
||||
double nz = dz / (double) r;
|
||||
double d = nx * nx + ny * ny + nz * nz;
|
||||
double wobble = (TreeFunctions.valueNoise3D(dx, dy, dz, seed) - 0.5) * 2.0 * rough;
|
||||
if (d <= 1.0 + wobble) {
|
||||
canvas.set(dx, dy, dz, CoralCanvas.Role.STRUCTURE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void buildPillar(CoralCanvas canvas, Coral coral, int height, long seed) {
|
||||
int r = Math.max(1, coral.pillarRadius());
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int dx = -r; dx <= r; dx++) {
|
||||
for (int dz = -r; dz <= r; dz++) {
|
||||
if (dx * dx + dz * dz <= r * r) {
|
||||
canvas.set(dx, y, dz, CoralCanvas.Role.STRUCTURE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
placeTipCluster(canvas, coral, 0, height - 1, 0, coral.tipClusterRadius(), seed);
|
||||
}
|
||||
|
||||
private static void buildTendril(CoralCanvas canvas, Coral coral, int height, long seed, RNG rng) {
|
||||
int count = Math.max(1, coral.tendrilCount());
|
||||
double radius = Math.max(0.0, coral.spread());
|
||||
for (int c = 0; c < count; c++) {
|
||||
double az = Math.toRadians(c * (360.0 / count));
|
||||
double bx = Math.cos(az) * radius * 0.5;
|
||||
double bz = Math.sin(az) * radius * 0.5;
|
||||
int tx = (int) Math.round(bx);
|
||||
int tz = (int) Math.round(bz);
|
||||
int top = 0;
|
||||
for (int y = 0; y < height; y++) {
|
||||
placeWavy(canvas, coral, tx, y, tz, y * (c + 1), seed + c * 311L, CoralCanvas.Role.STRUCTURE);
|
||||
top = y;
|
||||
}
|
||||
placeTip(canvas, coral, tx, top, tz, seed + c * 311L);
|
||||
}
|
||||
}
|
||||
|
||||
private static double[] rasterizeArm(CoralCanvas canvas, Coral coral, int ox, int oy, int oz,
|
||||
double azimuthDeg, double elevationDeg, double length, long seed) {
|
||||
double azRad = Math.toRadians(azimuthDeg);
|
||||
double elRad = Math.toRadians(elevationDeg);
|
||||
double dx = length * Math.cos(elRad) * Math.sin(azRad);
|
||||
double dy = length * Math.sin(elRad);
|
||||
double dz = length * Math.cos(elRad) * Math.cos(azRad);
|
||||
int steps = Math.max(1, (int) Math.round(Math.max(Math.abs(dy), Math.max(Math.abs(dx), Math.abs(dz)))));
|
||||
|
||||
double[] tip = new double[]{ox, oy, oz};
|
||||
for (int i = 0; i <= steps; i++) {
|
||||
double t = i / (double) steps;
|
||||
double sway = swayOffset(coral, t * length, seed);
|
||||
int x = (int) Math.round(ox + dx * t + sway);
|
||||
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};
|
||||
}
|
||||
return tip;
|
||||
}
|
||||
|
||||
private static void placeWavy(CoralCanvas canvas, Coral coral, int x, int y, int z, double phase, long seed, CoralCanvas.Role role) {
|
||||
double sway = swayOffset(coral, phase, seed);
|
||||
int sx = (int) Math.round(sway);
|
||||
int sz = (int) Math.round(swayOffset(coral, phase + 53.0, seed + 7L));
|
||||
canvas.set(x + sx, y, z + sz, role);
|
||||
}
|
||||
|
||||
private static double swayOffset(Coral coral, double phase, long seed) {
|
||||
double amount = Math.max(0.0, Math.min(1.0, coral.sway()));
|
||||
if (amount <= 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
double n = TreeFunctions.valueNoise1D(phase * 0.35, seed) - 0.5;
|
||||
return n * 2.0 * amount * (1.0 + coral.spread() * 0.25);
|
||||
}
|
||||
|
||||
private static void placeTipCluster(CoralCanvas canvas, Coral coral, int cx, int cy, int cz, int radius, long seed) {
|
||||
if (radius <= 0) {
|
||||
placeTip(canvas, coral, cx, cy, cz, seed);
|
||||
return;
|
||||
}
|
||||
for (int dx = -radius; dx <= radius; dx++) {
|
||||
for (int dy = -radius; dy <= radius; dy++) {
|
||||
for (int dz = -radius; dz <= radius; dz++) {
|
||||
if (dx * dx + dy * dy + dz * dz <= radius * radius) {
|
||||
canvas.set(cx + dx, cy + dy, cz + dz, CoralCanvas.Role.STRUCTURE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
placeTip(canvas, coral, cx, cy + radius + 1, cz, seed);
|
||||
}
|
||||
|
||||
private static void placeTip(CoralCanvas canvas, Coral coral, int x, int y, int z, long seed) {
|
||||
if (!hasTip(coral)) {
|
||||
return;
|
||||
}
|
||||
if (TreeFunctions.valueNoise1D(x * 31.0 + y * 17.0 + z * 13.0, seed) <= coral.tipChance()) {
|
||||
canvas.set(x, y, z, CoralCanvas.Role.TIP);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasTip(Coral coral) {
|
||||
return IrisProceduralBlocks.paletteSet(coral.tipPalette())
|
||||
|| (coral.tipBlock() != null && !coral.tipBlock().isEmpty());
|
||||
}
|
||||
|
||||
private static double branchAzimuth(Coral coral, int index, int count, RNG rng) {
|
||||
return switch (coral.branchAzimuth()) {
|
||||
case GOLDEN_ANGLE -> index * GOLDEN_ANGLE;
|
||||
case EVEN -> index * (360.0 / count);
|
||||
case RANDOM -> rng.d(0.0, 360.0);
|
||||
};
|
||||
}
|
||||
|
||||
private static Map<Vector3i, BlockData> resolve(CoralCanvas canvas, Coral coral, ObjectBuilder blocks) {
|
||||
Map<Vector3i, BlockData> out = new HashMap<>();
|
||||
RNG paletteRng = new RNG(coral.seed());
|
||||
for (Map.Entry<Long, CoralCanvas.Role> entry : canvas.getCells().entrySet()) {
|
||||
int[] xyz = CoralCanvas.decode(entry.getKey());
|
||||
int x = xyz[0];
|
||||
int y = xyz[1];
|
||||
int z = xyz[2];
|
||||
BlockData state;
|
||||
if (entry.getValue() == CoralCanvas.Role.TIP) {
|
||||
state = IrisProceduralBlocks.resolve(coral.tipBlock(), coral.tipPalette(), blocks, x, y, z, paletteRng);
|
||||
if (state == null) {
|
||||
state = IrisProceduralBlocks.resolve(coral.block(), coral.blockPalette(), blocks, x, y, z, paletteRng);
|
||||
}
|
||||
} else {
|
||||
state = IrisProceduralBlocks.resolve(coral.block(), coral.blockPalette(), blocks, x, y, z, paletteRng);
|
||||
}
|
||||
if (state == null) {
|
||||
continue;
|
||||
}
|
||||
if (coral.waterlogged() && IrisProceduralBlocks.hasProperty(state, "waterlogged")) {
|
||||
try {
|
||||
state = blocks.withProperty(state, "waterlogged", "true");
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
out.put(new Vector3i(x, y, z), state);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.coral;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBuilder;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralShape;
|
||||
import com.volmit.iris.engine.object.procedural.ResolvedProceduralPlacement;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
/** A coral reef made placeable: its variants are baked by {@link CoralGenerator}. */
|
||||
public final class ResolvedCoral extends ResolvedProceduralPlacement {
|
||||
private final Coral settings;
|
||||
|
||||
public ResolvedCoral(Coral settings, IrisData data) {
|
||||
super(new Shape("coral", settings.name(), settings.chance(), settings.density(), settings.seed(), settings.variants(), settings.mode(),
|
||||
settings.rotation(), settings.clamp(), settings.carvingSupport(), settings.underwater(), settings.translate(),
|
||||
settings.stiltSettings().orElse(null)), data);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ProceduralShape generate(int index, RNG rng, ObjectBuilder blocks) {
|
||||
return CoralGenerator.generate(settings, index, rng, blocks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.crystal;
|
||||
|
||||
import com.volmit.iris.engine.object.ObjectPlaceMode;
|
||||
import com.volmit.iris.engine.object.CarvingMode;
|
||||
import com.volmit.iris.engine.object.IrisObjectRotation;
|
||||
import com.volmit.iris.engine.object.IrisObjectTranslate;
|
||||
import com.volmit.iris.engine.object.IrisStiltSettings;
|
||||
import com.volmit.iris.engine.object.IrisObjectLimit;
|
||||
import com.volmit.iris.engine.object.IrisMaterialPalette;
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
import com.volmit.iris.engine.object.annotations.Snippet;
|
||||
import com.volmit.iris.engine.object.annotations.MaxNumber;
|
||||
import com.volmit.iris.engine.object.annotations.MinNumber;
|
||||
import com.volmit.iris.engine.object.annotations.RegistryListBlockType;
|
||||
import com.volmit.iris.engine.object.annotations.Required;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Snippet("crystal")
|
||||
@Desc("A single procedurally generated crystal cluster: a budding base blob with faceted, tapered shards radiating from it (geode / amethyst-style). Iris bakes a pool of deterministic variants from these settings and scatters them at world-gen time, exactly like an object placement but generated from scratch instead of loaded from an iob file. Built for caves.")
|
||||
public final class Crystal {
|
||||
@Desc("A human readable name used in logs and as the variant load key.")
|
||||
private String name = "crystal";
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The chance per chunk for this crystal cluster to attempt placement. Use density for multiple per chunk.")
|
||||
private double chance = 0.2;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("If the chance check passes, attempt this many placements in the chunk.")
|
||||
private int density = 1;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("How many distinct variants to pre-bake for this crystal cluster. Higher means more variety at a small memory cost.")
|
||||
private int variants = 6;
|
||||
|
||||
@Desc("The base seed for deterministic generation. The same seed and settings always bake the same variants.")
|
||||
private long seed = 1337;
|
||||
|
||||
@Desc("The placement mode used to anchor the crystal cluster to the terrain.")
|
||||
private ObjectPlaceMode mode = ObjectPlaceMode.CENTER_HEIGHT;
|
||||
|
||||
@Desc("Rotate this crystal cluster's placement.")
|
||||
private IrisObjectRotation rotation = new IrisObjectRotation();
|
||||
|
||||
@Desc("Limit the max or min height of placement.")
|
||||
private IrisObjectLimit clamp = new IrisObjectLimit();
|
||||
|
||||
@Desc("Whether this crystal cluster may place on the terrain surface, under carvings, or both. Crystals are meant for caves, so this defaults to CARVING_ONLY.")
|
||||
private CarvingMode carvingSupport = CarvingMode.CARVING_ONLY;
|
||||
|
||||
@Desc("If true, the crystal cluster anchors on the terrain height ignoring the water surface.")
|
||||
private boolean underwater = false;
|
||||
|
||||
@Desc("Translate (offset) this placement along each axis; for example set a negative y to sink it into the ground.")
|
||||
private IrisObjectTranslate translate = new IrisObjectTranslate();
|
||||
|
||||
@Desc("Settings for the stilt place modes (STILT, MIN_STILT, FAST_STILT, CENTER_STILT, ERODE_STILT, ORGANIC_STILT).")
|
||||
private IrisStiltSettings stiltSettings;
|
||||
|
||||
@Desc("The surface this crystal grows from (FLOOR shards up, CEILING shards down, WALL shards outward). This orients the baked geometry only.")
|
||||
private CrystalSurface growthSurface = CrystalSurface.FLOOR;
|
||||
|
||||
@Required
|
||||
@Desc("The primary crystal shard block, e.g. minecraft:amethyst_block. Ignored when blockPalette is set.")
|
||||
@RegistryListBlockType
|
||||
private String block = "minecraft:amethyst_block";
|
||||
|
||||
@Desc("A noise-driven palette for the crystal shards (a prismatic mix, e.g. amethyst_block, calcite, tinted_glass). When set this overrides the single block, letting the shards mix blocks by noise. Palette wins.")
|
||||
private IrisMaterialPalette blockPalette;
|
||||
|
||||
@Desc("Optional block placed at the very tip of each shard for a different colored or sparkling point (e.g. minecraft:amethyst_cluster or glowstone). Ignored when tipPalette is set. If unset and glow is true, a light-emitting block is sprinkled among the tips instead.")
|
||||
@RegistryListBlockType
|
||||
private String tipBlock = null;
|
||||
|
||||
@Desc("A noise-driven palette for the shard tips. When set this overrides the single tipBlock. Palette wins.")
|
||||
private IrisMaterialPalette tipPalette;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The chance per shard that its very tip is replaced by the tip block (or glow block).")
|
||||
private double tipChance = 0.6;
|
||||
|
||||
@Desc("If true and no tipBlock/tipPalette is set, sprinkle a light-emitting block (the glowBlock) among the shard tips for a sparkling geode.")
|
||||
private boolean glow = false;
|
||||
|
||||
@Desc("The light-emitting block sprinkled among the tips when glow is true and no tip block is configured.")
|
||||
@RegistryListBlockType
|
||||
private String glowBlock = "minecraft:glowstone";
|
||||
|
||||
@Desc("Optional block for the budding base blob the shards grow from, e.g. minecraft:budding_amethyst or calcite. Ignored when basePalette is set. If unset, the base is built from the primary shard block.")
|
||||
@RegistryListBlockType
|
||||
private String baseBlock = "minecraft:budding_amethyst";
|
||||
|
||||
@Desc("A noise-driven palette for the budding base blob. When set this overrides the single baseBlock. Palette wins.")
|
||||
private IrisMaterialPalette basePalette;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Radius in blocks of the budding base blob the shards radiate from. 0 places no base blob (shards spring from a single point).")
|
||||
private double baseRadius = 1.6;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("How strongly value noise perturbs the surface of the base blob, for a lumpy organic budding base rather than a clean sphere.")
|
||||
private double baseNoise = 0.35;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Minimum number of shards radiating from the base.")
|
||||
private int shardCountMin = 5;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Maximum number of shards radiating from the base.")
|
||||
private int shardCountMax = 11;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Minimum length in blocks of a shard, measured from the base out to its pointed tip.")
|
||||
private int shardLengthMin = 3;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Maximum length in blocks of a shard, measured from the base out to its pointed tip.")
|
||||
private int shardLengthMax = 8;
|
||||
|
||||
@MinNumber(0.5)
|
||||
@Desc("Starting radius in blocks of a shard at its thick base end. The shard tapers from this down to a single block at the tip.")
|
||||
private double shardBaseRadius = 1.4;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("How aggressively each shard narrows from base to tip. 0 keeps a near-constant column; 1 tapers fully to a sharp point at the very tip. Each shard always ends in a single pointed block.")
|
||||
private double shardTaper = 0.85;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(90)
|
||||
@Desc("The half-angle in degrees of the cone the shards fan within around the surface normal. 0 makes all shards parallel to the normal; larger values splay them outward into a starburst.")
|
||||
private double spreadAngle = 45;
|
||||
|
||||
@Desc("How the shard azimuths are distributed around the normal (RANDOM for a chaotic clump, GOLDEN_ANGLE for an evenly spaced rosette).")
|
||||
private CrystalDistribution distribution = CrystalDistribution.GOLDEN_ANGLE;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Per-shard angular randomness (0-1) added on top of the chosen distribution so the cluster never looks mechanically regular. 0 is perfectly distributed; 1 is heavily jittered.")
|
||||
private double jitter = 0.25;
|
||||
|
||||
public Crystal() {
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public double chance() {
|
||||
return chance;
|
||||
}
|
||||
|
||||
public int density() {
|
||||
return density;
|
||||
}
|
||||
|
||||
public int variants() {
|
||||
return variants;
|
||||
}
|
||||
|
||||
public long seed() {
|
||||
return seed;
|
||||
}
|
||||
|
||||
public ObjectPlaceMode mode() {
|
||||
return mode == null ? ObjectPlaceMode.CENTER_HEIGHT : mode;
|
||||
}
|
||||
|
||||
public IrisObjectRotation rotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public IrisObjectLimit clamp() {
|
||||
return clamp;
|
||||
}
|
||||
|
||||
public CarvingMode carvingSupport() {
|
||||
return carvingSupport == null ? CarvingMode.CARVING_ONLY : carvingSupport;
|
||||
}
|
||||
|
||||
public boolean underwater() {
|
||||
return underwater;
|
||||
}
|
||||
|
||||
public IrisObjectTranslate translate() {
|
||||
return translate;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisStiltSettings> stiltSettings() {
|
||||
return Optional.ofNullable(stiltSettings);
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
|
||||
public CrystalSurface growthSurface() {
|
||||
return growthSurface == null ? CrystalSurface.FLOOR : growthSurface;
|
||||
}
|
||||
|
||||
public String block() {
|
||||
return block;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisMaterialPalette> blockPalette() {
|
||||
return Optional.ofNullable(blockPalette);
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<String> tipBlock() {
|
||||
return Optional.ofNullable(tipBlock);
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisMaterialPalette> tipPalette() {
|
||||
return Optional.ofNullable(tipPalette);
|
||||
}
|
||||
|
||||
public double tipChance() {
|
||||
return tipChance;
|
||||
}
|
||||
|
||||
public boolean glow() {
|
||||
return glow;
|
||||
}
|
||||
|
||||
public String glowBlock() {
|
||||
return glowBlock;
|
||||
}
|
||||
|
||||
public String baseBlock() {
|
||||
return baseBlock;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisMaterialPalette> basePalette() {
|
||||
return Optional.ofNullable(basePalette);
|
||||
}
|
||||
|
||||
public double baseRadius() {
|
||||
return baseRadius;
|
||||
}
|
||||
|
||||
public double baseNoise() {
|
||||
return baseNoise;
|
||||
}
|
||||
|
||||
public int shardCountMin() {
|
||||
return shardCountMin;
|
||||
}
|
||||
|
||||
public int shardCountMax() {
|
||||
return shardCountMax;
|
||||
}
|
||||
|
||||
public int shardLengthMin() {
|
||||
return shardLengthMin;
|
||||
}
|
||||
|
||||
public int shardLengthMax() {
|
||||
return shardLengthMax;
|
||||
}
|
||||
|
||||
public double shardBaseRadius() {
|
||||
return shardBaseRadius;
|
||||
}
|
||||
|
||||
public double shardTaper() {
|
||||
return shardTaper;
|
||||
}
|
||||
|
||||
public double spreadAngle() {
|
||||
return spreadAngle;
|
||||
}
|
||||
|
||||
public CrystalDistribution distribution() {
|
||||
return distribution == null ? CrystalDistribution.GOLDEN_ANGLE : distribution;
|
||||
}
|
||||
|
||||
public double jitter() {
|
||||
return jitter;
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.crystal;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.tree.TreeFunctions;
|
||||
|
||||
public final class CrystalBaseBuilder {
|
||||
private CrystalBaseBuilder() {
|
||||
}
|
||||
|
||||
public static void build(CrystalCanvas canvas, Crystal crystal, long seed) {
|
||||
double radius = crystal.baseRadius();
|
||||
if (radius <= 0.0) {
|
||||
canvas.set(0, 0, 0, CrystalRole.BASE);
|
||||
return;
|
||||
}
|
||||
|
||||
double noiseStrength = crystal.baseNoise();
|
||||
int reach = (int) Math.ceil(radius + 1.0);
|
||||
for (int x = -reach; x <= reach; x++) {
|
||||
for (int y = -reach; y <= reach; y++) {
|
||||
for (int z = -reach; z <= reach; z++) {
|
||||
double distance = Math.sqrt((double) x * x + (double) y * y + (double) z * z);
|
||||
double wobble = (TreeFunctions.valueNoise3D(x, y, z, seed) - 0.5) * 2.0 * noiseStrength * radius;
|
||||
if (distance <= radius + wobble) {
|
||||
canvas.set(x, y, z, CrystalRole.BASE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.crystal;
|
||||
|
||||
import com.volmit.iris.util.math.Vector3i;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class CrystalCanvas {
|
||||
private final Map<Vector3i, CrystalRole> cells = new HashMap<>();
|
||||
|
||||
public void set(int x, int y, int z, CrystalRole role) {
|
||||
Vector3i key = new Vector3i(x, y, z);
|
||||
CrystalRole existing = cells.get(key);
|
||||
if (existing == null || rank(role) > rank(existing)) {
|
||||
cells.put(key, role);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean has(int x, int y, int z) {
|
||||
return cells.containsKey(new Vector3i(x, y, z));
|
||||
}
|
||||
|
||||
public Map<Vector3i, CrystalRole> getCells() {
|
||||
return cells;
|
||||
}
|
||||
|
||||
private static int rank(CrystalRole role) {
|
||||
return switch (role) {
|
||||
case BASE -> 0;
|
||||
case SHARD -> 1;
|
||||
case TIP -> 2;
|
||||
};
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.crystal;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("How the shards of a crystal cluster are angularly distributed around the surface normal.")
|
||||
public enum CrystalDistribution {
|
||||
@Desc("Each shard gets a uniformly random azimuth around the normal. Produces a chaotic, naturally clumped cluster.")
|
||||
RANDOM,
|
||||
|
||||
@Desc("Each shard's azimuth advances by the golden angle (~137.5 degrees) from the previous one. Produces an evenly spaced sunflower-like fan with no two shards overlapping, ideal for a geode rosette.")
|
||||
GOLDEN_ANGLE
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.crystal;
|
||||
|
||||
import com.volmit.iris.engine.object.IrisMaterialPalette;
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBuilder;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralShape;
|
||||
import com.volmit.iris.engine.object.procedural.IrisProceduralBlocks;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import com.volmit.iris.util.math.Vector3i;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class CrystalGenerator {
|
||||
private CrystalGenerator() {
|
||||
}
|
||||
|
||||
public static ProceduralShape generate(Crystal crystal, int variantIndex, RNG rng, ObjectBuilder blocks) {
|
||||
CrystalCanvas canvas = new CrystalCanvas();
|
||||
long structureSeed = rng.getSeed();
|
||||
|
||||
CrystalBaseBuilder.build(canvas, crystal, structureSeed + 977L);
|
||||
CrystalShardBuilder.build(canvas, crystal, rng);
|
||||
|
||||
Map<Vector3i, BlockData> resolved = new HashMap<>();
|
||||
RNG paletteRng = new RNG(crystal.seed());
|
||||
RNG tipRng = new RNG(crystal.seed() + (variantIndex * 31337L) + 4099L);
|
||||
|
||||
for (Map.Entry<Vector3i, CrystalRole> entry : canvas.getCells().entrySet()) {
|
||||
Vector3i position = entry.getKey();
|
||||
int x = position.getBlockX();
|
||||
int y = position.getBlockY();
|
||||
int z = position.getBlockZ();
|
||||
BlockData blockData = resolveRole(crystal, entry.getValue(), blocks, x, y, z, paletteRng, tipRng);
|
||||
if (blockData == null) {
|
||||
continue;
|
||||
}
|
||||
resolved.put(position, blockData);
|
||||
}
|
||||
|
||||
return IrisProceduralBlocks.assemble(resolved);
|
||||
}
|
||||
|
||||
private static BlockData resolveRole(Crystal crystal, CrystalRole role, ObjectBuilder blocks, int x, int y, int z, RNG paletteRng, RNG tipRng) {
|
||||
return switch (role) {
|
||||
case BASE -> resolveBase(crystal, blocks, x, y, z, paletteRng);
|
||||
case SHARD -> resolveShard(crystal, blocks, x, y, z, paletteRng);
|
||||
case TIP -> resolveTip(crystal, blocks, x, y, z, paletteRng, tipRng);
|
||||
};
|
||||
}
|
||||
|
||||
private static BlockData resolveBase(Crystal crystal, ObjectBuilder blocks, int x, int y, int z, RNG paletteRng) {
|
||||
boolean hasBase = IrisProceduralBlocks.paletteSet(crystal.basePalette())
|
||||
|| (crystal.baseBlock() != null && !crystal.baseBlock().isEmpty());
|
||||
if (hasBase) {
|
||||
BlockData base = IrisProceduralBlocks.resolve(crystal.baseBlock(), crystal.basePalette(), blocks, x, y, z, paletteRng);
|
||||
if (base != null) {
|
||||
return base;
|
||||
}
|
||||
}
|
||||
return resolveShard(crystal, blocks, x, y, z, paletteRng);
|
||||
}
|
||||
|
||||
private static BlockData resolveShard(Crystal crystal, ObjectBuilder blocks, int x, int y, int z, RNG paletteRng) {
|
||||
return IrisProceduralBlocks.resolve(crystal.block(), crystal.blockPalette(), blocks, x, y, z, paletteRng);
|
||||
}
|
||||
|
||||
private static BlockData resolveTip(Crystal crystal, ObjectBuilder blocks, int x, int y, int z, RNG paletteRng, RNG tipRng) {
|
||||
boolean hasTip = IrisProceduralBlocks.paletteSet(crystal.tipPalette())
|
||||
|| (crystal.tipBlock() != null && !crystal.tipBlock().isEmpty());
|
||||
|
||||
if (hasTip) {
|
||||
if (tipRng.chance(crystal.tipChance())) {
|
||||
BlockData tip = IrisProceduralBlocks.resolve(crystal.tipBlock(), crystal.tipPalette(), blocks, x, y, z, paletteRng);
|
||||
if (tip != null) {
|
||||
return tip;
|
||||
}
|
||||
}
|
||||
return resolveShard(crystal, blocks, x, y, z, paletteRng);
|
||||
}
|
||||
|
||||
if (crystal.glow() && crystal.glowBlock() != null && !crystal.glowBlock().isEmpty()) {
|
||||
if (tipRng.chance(crystal.tipChance())) {
|
||||
BlockData glow = IrisProceduralBlocks.resolve(crystal.glowBlock(), (IrisMaterialPalette) null, blocks, x, y, z, paletteRng);
|
||||
if (glow != null) {
|
||||
return glow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resolveShard(crystal, blocks, x, y, z, paletteRng);
|
||||
}
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.crystal;
|
||||
|
||||
|
||||
|
||||
public final class CrystalGeometry {
|
||||
private CrystalGeometry() {
|
||||
}
|
||||
|
||||
public static double[] surfaceNormal(CrystalSurface surface) {
|
||||
return switch (surface) {
|
||||
case FLOOR -> new double[]{0.0, 1.0, 0.0};
|
||||
case CEILING -> new double[]{0.0, -1.0, 0.0};
|
||||
case WALL -> {
|
||||
double len = Math.sqrt(1.0 + 0.5 * 0.5);
|
||||
yield new double[]{1.0 / len, 0.5 / len, 0.0};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static double[] orthonormalBasisU(double[] normal) {
|
||||
double[] reference = Math.abs(normal[1]) > 0.99 ? new double[]{1.0, 0.0, 0.0} : new double[]{0.0, 1.0, 0.0};
|
||||
double[] u = cross(reference, normal);
|
||||
normalize(u);
|
||||
return u;
|
||||
}
|
||||
|
||||
public static double[] orthonormalBasisV(double[] normal, double[] u) {
|
||||
double[] v = cross(normal, u);
|
||||
normalize(v);
|
||||
return v;
|
||||
}
|
||||
|
||||
public static double[] coneDirection(double[] normal, double[] u, double[] v, double coneAngleRadians, double azimuthRadians) {
|
||||
double sin = Math.sin(coneAngleRadians);
|
||||
double cos = Math.cos(coneAngleRadians);
|
||||
double ca = Math.cos(azimuthRadians);
|
||||
double sa = Math.sin(azimuthRadians);
|
||||
double[] dir = new double[3];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
dir[i] = cos * normal[i] + sin * (ca * u[i] + sa * v[i]);
|
||||
}
|
||||
normalize(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
public static double[] cross(double[] a, double[] b) {
|
||||
return new double[]{
|
||||
a[1] * b[2] - a[2] * b[1],
|
||||
a[2] * b[0] - a[0] * b[2],
|
||||
a[0] * b[1] - a[1] * b[0]
|
||||
};
|
||||
}
|
||||
|
||||
public static void normalize(double[] vec) {
|
||||
double len = Math.sqrt(vec[0] * vec[0] + vec[1] * vec[1] + vec[2] * vec[2]);
|
||||
if (len < 1e-9) {
|
||||
vec[0] = 0.0;
|
||||
vec[1] = 1.0;
|
||||
vec[2] = 0.0;
|
||||
return;
|
||||
}
|
||||
vec[0] /= len;
|
||||
vec[1] /= len;
|
||||
vec[2] /= len;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.crystal;
|
||||
|
||||
public enum CrystalRole {
|
||||
BASE,
|
||||
SHARD,
|
||||
TIP
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.crystal;
|
||||
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
public final class CrystalShardBuilder {
|
||||
private static final double GOLDEN_ANGLE = 137.50776405003785;
|
||||
|
||||
private CrystalShardBuilder() {
|
||||
}
|
||||
|
||||
public static void build(CrystalCanvas canvas, Crystal crystal, RNG rng) {
|
||||
double[] normal = CrystalGeometry.surfaceNormal(crystal.growthSurface());
|
||||
double[] u = CrystalGeometry.orthonormalBasisU(normal);
|
||||
double[] v = CrystalGeometry.orthonormalBasisV(normal, u);
|
||||
|
||||
int countLo = Math.min(crystal.shardCountMin(), crystal.shardCountMax());
|
||||
int countHi = Math.max(crystal.shardCountMin(), crystal.shardCountMax());
|
||||
int shardCount = Math.max(1, rng.i(countLo, countHi + 1));
|
||||
|
||||
int lengthLo = Math.min(crystal.shardLengthMin(), crystal.shardLengthMax());
|
||||
int lengthHi = Math.max(crystal.shardLengthMin(), crystal.shardLengthMax());
|
||||
|
||||
double spreadRadians = Math.toRadians(Math.max(0.0, Math.min(90.0, crystal.spreadAngle())));
|
||||
double jitterAmount = Math.max(0.0, Math.min(1.0, crystal.jitter()));
|
||||
double emitRadius = Math.max(0.0, crystal.baseRadius() * 0.6);
|
||||
|
||||
for (int i = 0; i < shardCount; i++) {
|
||||
double azimuth = azimuth(crystal.distribution(), i, rng);
|
||||
azimuth += rng.d(-Math.PI, Math.PI) * jitterAmount * 0.5;
|
||||
|
||||
double cone = spreadRadians * fanFraction(shardCount, i, rng);
|
||||
cone += rng.d(-spreadRadians, spreadRadians) * jitterAmount * 0.5;
|
||||
cone = Math.max(0.0, Math.min(Math.PI / 2.0, cone));
|
||||
|
||||
double[] direction = CrystalGeometry.coneDirection(normal, u, v, cone, azimuth);
|
||||
|
||||
double ox = normal[0] * emitRadius + u[0] * Math.cos(azimuth) * emitRadius + v[0] * Math.sin(azimuth) * emitRadius;
|
||||
double oy = normal[1] * emitRadius + u[1] * Math.cos(azimuth) * emitRadius + v[1] * Math.sin(azimuth) * emitRadius;
|
||||
double oz = normal[2] * emitRadius + u[2] * Math.cos(azimuth) * emitRadius + v[2] * Math.sin(azimuth) * emitRadius;
|
||||
|
||||
int length = Math.max(1, rng.i(lengthLo, lengthHi + 1));
|
||||
rasterizeShard(canvas, crystal, ox, oy, oz, direction, length);
|
||||
}
|
||||
}
|
||||
|
||||
private static void rasterizeShard(CrystalCanvas canvas, Crystal crystal, double ox, double oy, double oz, double[] direction, int length) {
|
||||
double baseRadius = Math.max(0.5, crystal.shardBaseRadius());
|
||||
double taper = Math.max(0.0, Math.min(1.0, crystal.shardTaper()));
|
||||
|
||||
int samples = Math.max(1, length * 3);
|
||||
|
||||
for (int s = 0; s <= samples; s++) {
|
||||
double t = s / (double) samples;
|
||||
double cx = ox + direction[0] * length * t;
|
||||
double cy = oy + direction[1] * length * t;
|
||||
double cz = oz + direction[2] * length * t;
|
||||
|
||||
double radius = baseRadius * (1.0 - taper * t);
|
||||
if (t >= 1.0) {
|
||||
radius = 0.0;
|
||||
}
|
||||
radius = Math.max(0.0, radius);
|
||||
|
||||
int reach = (int) Math.floor(radius);
|
||||
int centerX = (int) Math.round(cx);
|
||||
int centerY = (int) Math.round(cy);
|
||||
int centerZ = (int) Math.round(cz);
|
||||
|
||||
if (reach <= 0) {
|
||||
canvas.set(centerX, centerY, centerZ, CrystalRole.SHARD);
|
||||
} else {
|
||||
for (int dx = -reach; dx <= reach; dx++) {
|
||||
for (int dy = -reach; dy <= reach; dy++) {
|
||||
for (int dz = -reach; dz <= reach; dz++) {
|
||||
double dist = Math.sqrt((double) dx * dx + (double) dy * dy + (double) dz * dz);
|
||||
if (dist <= radius) {
|
||||
canvas.set(centerX + dx, centerY + dy, centerZ + dz, CrystalRole.SHARD);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int tipX = (int) Math.round(ox + direction[0] * length);
|
||||
int tipY = (int) Math.round(oy + direction[1] * length);
|
||||
int tipZ = (int) Math.round(oz + direction[2] * length);
|
||||
canvas.set(tipX, tipY, tipZ, CrystalRole.TIP);
|
||||
}
|
||||
|
||||
private static double azimuth(CrystalDistribution distribution, int index, RNG rng) {
|
||||
if (distribution == CrystalDistribution.GOLDEN_ANGLE) {
|
||||
return Math.toRadians(index * GOLDEN_ANGLE);
|
||||
}
|
||||
return rng.d(0.0, Math.PI * 2.0);
|
||||
}
|
||||
|
||||
private static double fanFraction(int count, int index, RNG rng) {
|
||||
if (count <= 1) {
|
||||
return rng.d(0.0, 1.0);
|
||||
}
|
||||
double base = index / (double) (count - 1);
|
||||
return Math.max(0.0, Math.min(1.0, base * 0.7 + rng.d(0.0, 0.3)));
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.crystal;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("The surface a crystal cluster grows from. This only orients the baked geometry (which way the shards point); placement of the object into a cave is handled by the placement system, not here.")
|
||||
public enum CrystalSurface {
|
||||
@Desc("Grows from a cave floor. The budding base sits at the bottom and the shards point upward, fanning within the spread cone around the up axis. Built with positive py so the cluster grows up from the base layer.")
|
||||
FLOOR,
|
||||
|
||||
@Desc("Grows from a cave ceiling. The budding base sits at the top and the shards point downward like stalactites, fanning within the spread cone around the down axis. Built with negative py so the cluster grows down from the ceiling.")
|
||||
CEILING,
|
||||
|
||||
@Desc("Grows from a cave wall. The budding base hugs the wall and the shards point outward and slightly upward, fanning within the spread cone around a tilted normal so the cluster reads as a wall sconce of shards.")
|
||||
WALL
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.crystal;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBuilder;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralShape;
|
||||
import com.volmit.iris.engine.object.procedural.ResolvedProceduralPlacement;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
/** A crystal cluster made placeable: its variants are baked by {@link CrystalGenerator}. */
|
||||
public final class ResolvedCrystal extends ResolvedProceduralPlacement {
|
||||
private final Crystal settings;
|
||||
|
||||
public ResolvedCrystal(Crystal settings, IrisData data) {
|
||||
super(new Shape("crystal", settings.name(), settings.chance(), settings.density(), settings.seed(), settings.variants(), settings.mode(),
|
||||
settings.rotation(), settings.clamp(), settings.carvingSupport(), settings.underwater(), settings.translate(),
|
||||
settings.stiltSettings().orElse(null)), data);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ProceduralShape generate(int index, RNG rng, ObjectBuilder blocks) {
|
||||
return CrystalGenerator.generate(settings, index, rng, blocks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,439 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.formation;
|
||||
|
||||
import com.volmit.iris.engine.object.ObjectPlaceMode;
|
||||
import com.volmit.iris.engine.object.CarvingMode;
|
||||
import com.volmit.iris.engine.object.IrisObjectRotation;
|
||||
import com.volmit.iris.engine.object.IrisObjectTranslate;
|
||||
import com.volmit.iris.engine.object.IrisStiltSettings;
|
||||
import com.volmit.iris.engine.object.IrisObjectLimit;
|
||||
import com.volmit.iris.engine.object.IrisMaterialPalette;
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
import com.volmit.iris.engine.object.annotations.Snippet;
|
||||
import com.volmit.iris.engine.object.annotations.MaxNumber;
|
||||
import com.volmit.iris.engine.object.annotations.MinNumber;
|
||||
import com.volmit.iris.engine.object.annotations.RegistryListBlockType;
|
||||
import com.volmit.iris.engine.object.annotations.Required;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Snippet("formation")
|
||||
@Desc("A single procedurally generated natural formation, including rock landmarks and magical glacial silhouettes. Iris bakes a pool of deterministic variants from these settings and scatters them at world-gen time, exactly like an object placement but generated from scratch instead of loaded from an iob file.")
|
||||
public final class Formation {
|
||||
@Desc("A human readable name used in logs and as the variant load key.")
|
||||
private String name = "formation";
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The chance per chunk for this formation to attempt placement. Use density for multiple per chunk.")
|
||||
private double chance = 0.02;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("If the chance check passes, attempt this many placements in the chunk.")
|
||||
private int density = 1;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("How many distinct variants to pre-bake for this formation. Higher means more variety at a small memory cost.")
|
||||
private int variants = 6;
|
||||
|
||||
@Desc("The base seed for deterministic generation. The same seed and settings always bake the same variants.")
|
||||
private long seed = 1337;
|
||||
|
||||
@Desc("The placement mode used to anchor the formation to the terrain.")
|
||||
private ObjectPlaceMode mode = ObjectPlaceMode.CENTER_HEIGHT;
|
||||
|
||||
@Desc("Rotate this formation's placement.")
|
||||
private IrisObjectRotation rotation = new IrisObjectRotation();
|
||||
|
||||
@Desc("Limit the max or min height of placement.")
|
||||
private IrisObjectLimit clamp = new IrisObjectLimit();
|
||||
|
||||
@Desc("Whether this formation may place on the terrain surface, under carvings, or both.")
|
||||
private CarvingMode carvingSupport = CarvingMode.SURFACE_ONLY;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("Horizontal buffer, in blocks, beyond each lowest solid foundation block that must also be solid, un-carved ground.")
|
||||
private int surfaceSupportBuffer = 3;
|
||||
|
||||
@Desc("If true, the formation anchors on the terrain height ignoring the water surface.")
|
||||
private boolean underwater = false;
|
||||
|
||||
@Desc("Translate (offset) this placement along each axis; for example set a negative y to sink it into the ground.")
|
||||
private IrisObjectTranslate translate = new IrisObjectTranslate();
|
||||
|
||||
@Desc("Settings for the stilt place modes (STILT, MIN_STILT, FAST_STILT, CENTER_STILT, ERODE_STILT, ORGANIC_STILT).")
|
||||
private IrisStiltSettings stiltSettings;
|
||||
|
||||
@Desc("The overall silhouette of the formation. Drives which sculpting routine the generator uses.")
|
||||
private FormationForm form = FormationForm.SPIRE;
|
||||
|
||||
@Required
|
||||
@Desc("The main rock block, e.g. minecraft:stone. Ignored when blockPalette is set.")
|
||||
@RegistryListBlockType
|
||||
private String block = "minecraft:stone";
|
||||
|
||||
@Desc("A noise-driven palette for the main rock body. When set this overrides the single block, letting the body mix blocks by noise.")
|
||||
private IrisMaterialPalette blockPalette;
|
||||
|
||||
@Desc("Optional caprock block placed on the top crown of the formation (and the wide overhanging cap for HOODOO). Ignored when capPalette is set. When null and capPalette is unset, the formation uses its main rock everywhere.")
|
||||
@RegistryListBlockType
|
||||
private String capBlock = null;
|
||||
|
||||
@Desc("A noise-driven palette for the caprock. When set this overrides the single capBlock.")
|
||||
private IrisMaterialPalette capPalette;
|
||||
|
||||
@Desc("Optional palette of strata (horizontal colored bands) blended into the rock body by y level. When set, every strataThickness blocks of height switches to the next strata block, producing the signature banded look. Falls back to the main block where unset.")
|
||||
private IrisMaterialPalette strataPalette;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(32)
|
||||
@Desc("The thickness in blocks of each horizontal strata band when strataPalette is set.")
|
||||
private int strataThickness = 3;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Minimum total height of the formation in blocks.")
|
||||
private int heightMin = 14;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Maximum total height of the formation in blocks.")
|
||||
private int heightMax = 26;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Minimum base radius (half-width) of the formation in blocks.")
|
||||
private int baseWidthMin = 3;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Maximum base radius (half-width) of the formation in blocks.")
|
||||
private int baseWidthMax = 6;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("The target radius (half-width) at the very top of the formation in blocks, before the profile is applied. 0 means taper to a point.")
|
||||
private int topWidth = 0;
|
||||
|
||||
@Desc("The function used to shape the formation radius over its height.")
|
||||
private FormationProfile profile = FormationProfile.TAPER;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The normalized height (0 base, 1 top) of the pinched waist for the PARABOLIC profile (used by HOODOO).")
|
||||
private double profileWaist = 0.55;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The minimum radius fraction at the waist for the PARABOLIC profile. Lower values pinch the waist tighter.")
|
||||
private double profileWaistFloor = 0.35;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("How far the formation leans, in degrees from vertical. 0 is perfectly upright. The whole body is sheared by this amount over its height.")
|
||||
private double lean = 0;
|
||||
|
||||
@Desc("Compass direction in degrees the formation leans toward when lean is non-zero.")
|
||||
private double leanAzimuth = 0;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Surface roughness from 0 (a clean smooth solid) to 1 (heavily eroded and pitted). Driven by 3D value noise that perturbs the radius so the formation never reads as a clean cylinder.")
|
||||
private double roughness = 0.3;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Per-block surface jitter from 0 (none) to 1 (maximum). Randomly carves and adds isolated edge blocks to break up the silhouette.")
|
||||
private double jitter = 0.15;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Extra wide cap radius in blocks added on top for the HOODOO form, producing the overhanging mushroom caprock. 0 disables the overhang.")
|
||||
private int hoodooCapRadius = 3;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(6)
|
||||
@Desc("The height in blocks of the HOODOO overhanging caprock slab.")
|
||||
private int hoodooCapHeight = 3;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("How wide the ARCH opening is in blocks (the gap between the two legs). The arch span is rasterized over this width.")
|
||||
private int archSpan = 10;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("The thickness in blocks of the ARCH legs and spanning curve.")
|
||||
private int archThickness = 3;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("How strongly an ARCH varies its leg steepness, crown position, depth bow and tube width between deterministic variants. 0 is symmetric and 1 is highly organic.")
|
||||
private double archAsymmetry = 0.35;
|
||||
|
||||
@MinNumber(2)
|
||||
@MaxNumber(12)
|
||||
@Desc("How many separate columns make up a BASALT_COLUMN cluster. Each column gets a randomized height around the formation height range.")
|
||||
private int basaltColumns = 5;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("The radius (half-width) in blocks of each individual column in a BASALT_COLUMN cluster.")
|
||||
private int basaltColumnRadius = 1;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("How much the heights of individual BASALT_COLUMN columns vary from each other, from 0 (all equal) to 1 (highly varied).")
|
||||
private double basaltHeightVariance = 0.45;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(12)
|
||||
@Desc("How many tapered summits crown an ICEBERG formation.")
|
||||
private int icebergPeaks = 3;
|
||||
|
||||
@MinNumber(2)
|
||||
@MaxNumber(8)
|
||||
@Desc("How many separated shards form a FISSURE.")
|
||||
private int fractureCount = 3;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(16)
|
||||
@Desc("Open-air separation in blocks between the shards of a FISSURE.")
|
||||
private int fractureSeparation = 2;
|
||||
|
||||
@MinNumber(0.25)
|
||||
@MaxNumber(6)
|
||||
@Desc("How many complete turns a SPIRAL makes from base to tip.")
|
||||
private double spiralTurns = 1.5;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(32)
|
||||
@Desc("The starting distance in blocks between a SPIRAL and its open center.")
|
||||
private int spiralRadius = 4;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(8)
|
||||
@Desc("The radius in blocks of the tube swept along a SPIRAL.")
|
||||
private int spiralThickness = 2;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(32)
|
||||
@Desc("How far the hooked arm of an OVERHANG projects from its base.")
|
||||
private int overhangReach = 8;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(16)
|
||||
@Desc("How many blocks the tip of an OVERHANG curls downward.")
|
||||
private int overhangDrop = 3;
|
||||
|
||||
public Formation() {
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public double chance() {
|
||||
return chance;
|
||||
}
|
||||
|
||||
public int density() {
|
||||
return density;
|
||||
}
|
||||
|
||||
public int variants() {
|
||||
return variants;
|
||||
}
|
||||
|
||||
public long seed() {
|
||||
return seed;
|
||||
}
|
||||
|
||||
public ObjectPlaceMode mode() {
|
||||
return mode == null ? ObjectPlaceMode.CENTER_HEIGHT : mode;
|
||||
}
|
||||
|
||||
public IrisObjectRotation rotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public IrisObjectLimit clamp() {
|
||||
return clamp;
|
||||
}
|
||||
|
||||
public CarvingMode carvingSupport() {
|
||||
return carvingSupport == null ? CarvingMode.SURFACE_ONLY : carvingSupport;
|
||||
}
|
||||
|
||||
public int surfaceSupportBuffer() {
|
||||
return surfaceSupportBuffer;
|
||||
}
|
||||
|
||||
public boolean underwater() {
|
||||
return underwater;
|
||||
}
|
||||
|
||||
public IrisObjectTranslate translate() {
|
||||
return translate;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisStiltSettings> stiltSettings() {
|
||||
return Optional.ofNullable(stiltSettings);
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
|
||||
public FormationForm form() {
|
||||
return form == null ? FormationForm.SPIRE : form;
|
||||
}
|
||||
|
||||
public String block() {
|
||||
return block;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisMaterialPalette> blockPalette() {
|
||||
return Optional.ofNullable(blockPalette);
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<String> capBlock() {
|
||||
return Optional.ofNullable(capBlock);
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisMaterialPalette> capPalette() {
|
||||
return Optional.ofNullable(capPalette);
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisMaterialPalette> strataPalette() {
|
||||
return Optional.ofNullable(strataPalette);
|
||||
}
|
||||
|
||||
public int strataThickness() {
|
||||
return strataThickness;
|
||||
}
|
||||
|
||||
public int heightMin() {
|
||||
return heightMin;
|
||||
}
|
||||
|
||||
public int heightMax() {
|
||||
return heightMax;
|
||||
}
|
||||
|
||||
public int baseWidthMin() {
|
||||
return baseWidthMin;
|
||||
}
|
||||
|
||||
public int baseWidthMax() {
|
||||
return baseWidthMax;
|
||||
}
|
||||
|
||||
public int topWidth() {
|
||||
return topWidth;
|
||||
}
|
||||
|
||||
public FormationProfile profile() {
|
||||
return profile == null ? FormationProfile.TAPER : profile;
|
||||
}
|
||||
|
||||
public double profileWaist() {
|
||||
return profileWaist;
|
||||
}
|
||||
|
||||
public double profileWaistFloor() {
|
||||
return profileWaistFloor;
|
||||
}
|
||||
|
||||
public double lean() {
|
||||
return lean;
|
||||
}
|
||||
|
||||
public double leanAzimuth() {
|
||||
return leanAzimuth;
|
||||
}
|
||||
|
||||
public double roughness() {
|
||||
return roughness;
|
||||
}
|
||||
|
||||
public double jitter() {
|
||||
return jitter;
|
||||
}
|
||||
|
||||
public int hoodooCapRadius() {
|
||||
return hoodooCapRadius;
|
||||
}
|
||||
|
||||
public int hoodooCapHeight() {
|
||||
return hoodooCapHeight;
|
||||
}
|
||||
|
||||
public int archSpan() {
|
||||
return archSpan;
|
||||
}
|
||||
|
||||
public int archThickness() {
|
||||
return archThickness;
|
||||
}
|
||||
|
||||
public double archAsymmetry() {
|
||||
return archAsymmetry;
|
||||
}
|
||||
|
||||
public int basaltColumns() {
|
||||
return basaltColumns;
|
||||
}
|
||||
|
||||
public int basaltColumnRadius() {
|
||||
return basaltColumnRadius;
|
||||
}
|
||||
|
||||
public double basaltHeightVariance() {
|
||||
return basaltHeightVariance;
|
||||
}
|
||||
|
||||
public int icebergPeaks() {
|
||||
return icebergPeaks;
|
||||
}
|
||||
|
||||
public int fractureCount() {
|
||||
return fractureCount;
|
||||
}
|
||||
|
||||
public int fractureSeparation() {
|
||||
return fractureSeparation;
|
||||
}
|
||||
|
||||
public double spiralTurns() {
|
||||
return spiralTurns;
|
||||
}
|
||||
|
||||
public int spiralRadius() {
|
||||
return spiralRadius;
|
||||
}
|
||||
|
||||
public int spiralThickness() {
|
||||
return spiralThickness;
|
||||
}
|
||||
|
||||
public int overhangReach() {
|
||||
return overhangReach;
|
||||
}
|
||||
|
||||
public int overhangDrop() {
|
||||
return overhangDrop;
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.formation;
|
||||
|
||||
import com.volmit.iris.engine.object.IrisMaterialPalette;
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBuilder;
|
||||
import com.volmit.iris.engine.object.procedural.IrisProceduralBlocks;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import com.volmit.iris.util.math.Vector3i;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
public final class FormationBlockResolver {
|
||||
private FormationBlockResolver() {
|
||||
}
|
||||
|
||||
public static BlockData resolve(Formation f, ObjectBuilder blocks, FormationCanvas.Role role, Vector3i raw) {
|
||||
int x = raw.getBlockX();
|
||||
int y = raw.getBlockY();
|
||||
int z = raw.getBlockZ();
|
||||
RNG paletteRng = new RNG(f.seed());
|
||||
|
||||
if (role == FormationCanvas.Role.CAP && capDefined(f)) {
|
||||
BlockData cap = IrisProceduralBlocks.resolve(f.capBlock(), f.capPalette(), blocks, x, y, z, paletteRng);
|
||||
if (cap != null) {
|
||||
return cap;
|
||||
}
|
||||
}
|
||||
|
||||
if (strataDefined(f)) {
|
||||
IrisMaterialPalette strata = f.strataPalette().orElseThrow();
|
||||
int thickness = Math.max(1, f.strataThickness());
|
||||
int band = Math.floorDiv(y, thickness);
|
||||
BlockData strataState = blocks.pick(strata, f.seed() + (band * 31L), x, band, z);
|
||||
if (strataState != null) {
|
||||
return strataState;
|
||||
}
|
||||
}
|
||||
|
||||
return IrisProceduralBlocks.resolve(f.block(), f.blockPalette(), blocks, x, y, z, paletteRng);
|
||||
}
|
||||
|
||||
private static boolean capDefined(Formation f) {
|
||||
return IrisProceduralBlocks.paletteSet(f.capPalette()) || f.capBlock().filter(block -> !block.isEmpty()).isPresent();
|
||||
}
|
||||
|
||||
private static boolean strataDefined(Formation f) {
|
||||
return IrisProceduralBlocks.paletteSet(f.strataPalette());
|
||||
}
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.formation;
|
||||
|
||||
import com.volmit.iris.util.math.Vector3i;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class FormationCanvas {
|
||||
public enum Role {
|
||||
BODY,
|
||||
CAP
|
||||
}
|
||||
|
||||
private final Map<Vector3i, Role> cells = new HashMap<>();
|
||||
|
||||
public void setBody(int x, int y, int z) {
|
||||
Vector3i key = new Vector3i(x, y, z);
|
||||
if (cells.get(key) == Role.CAP) {
|
||||
return;
|
||||
}
|
||||
cells.put(key, Role.BODY);
|
||||
}
|
||||
|
||||
public void setCap(int x, int y, int z) {
|
||||
cells.put(new Vector3i(x, y, z), Role.CAP);
|
||||
}
|
||||
|
||||
public void remove(int x, int y, int z) {
|
||||
cells.remove(new Vector3i(x, y, z));
|
||||
}
|
||||
|
||||
public boolean has(int x, int y, int z) {
|
||||
return cells.containsKey(new Vector3i(x, y, z));
|
||||
}
|
||||
|
||||
public Map<Vector3i, Role> getCells() {
|
||||
return cells;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return cells.isEmpty();
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.formation;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("The overall silhouette of a procedural natural formation. Each form drives a distinct sculpting routine in the formation generator.")
|
||||
public enum FormationForm {
|
||||
@Desc("A tall, slender needle of rock that tapers smoothly to a point at the top (a sharp pinnacle or stone spire).")
|
||||
SPIRE,
|
||||
@Desc("A column with a pinched, eroded waist and a wide overhanging caprock balanced on top (the classic desert hoodoo / mushroom rock).")
|
||||
HOODOO,
|
||||
@Desc("Two stout legs joined by a spanning curved bridge of rock overhead (a natural stone arch).")
|
||||
ARCH,
|
||||
@Desc("A chunky, blocky stack that tapers gently as it rises, like an isolated pillar of rock standing in water (a sea stack).")
|
||||
SEA_STACK,
|
||||
@Desc("A rounded, lumpy boulder formed from a noise-perturbed ellipsoid that sits low on the terrain.")
|
||||
BOULDER,
|
||||
@Desc("A tightly packed cluster of several vertical, near-hexagonal columns of varying height (a basalt column formation / giant's causeway).")
|
||||
BASALT_COLUMN,
|
||||
@Desc("A broad, faceted mound crowned by several irregular tapered peaks, suitable for icebergs and glacial outcrops.")
|
||||
ICEBERG,
|
||||
@Desc("Several separated, outward-leaning shards divided by open cracks.")
|
||||
FISSURE,
|
||||
@Desc("A tapered tube swept around a tightening helix, leaving open air through its curled center.")
|
||||
SPIRAL,
|
||||
@Desc("A grounded pillar that bends into a long hooked cantilever and curls downward at its tip.")
|
||||
OVERHANG
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.formation;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBuilder;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralShape;
|
||||
import com.volmit.iris.engine.object.procedural.IrisProceduralBlocks;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import com.volmit.iris.util.math.Vector3i;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class FormationGenerator {
|
||||
private FormationGenerator() {
|
||||
}
|
||||
|
||||
public static ProceduralShape generate(Formation f, int variantIndex, RNG rng, ObjectBuilder blocks) {
|
||||
int lo = Math.min(f.heightMin(), f.heightMax());
|
||||
int hi = Math.max(f.heightMin(), f.heightMax());
|
||||
int height = Math.max(3, rng.i(lo, hi + 1));
|
||||
|
||||
int wLo = Math.min(f.baseWidthMin(), f.baseWidthMax());
|
||||
int wHi = Math.max(f.baseWidthMin(), f.baseWidthMax());
|
||||
double baseRadius = Math.max(1, rng.i(wLo, wHi + 1));
|
||||
|
||||
FormationCanvas canvas = new FormationCanvas();
|
||||
|
||||
switch (f.form()) {
|
||||
case SPIRE -> FormationShapeBuilder.spire(canvas, f, height, baseRadius, rng);
|
||||
case HOODOO -> FormationShapeBuilder.hoodoo(canvas, f, height, baseRadius, rng);
|
||||
case ARCH -> FormationShapeBuilder.arch(canvas, f, height, baseRadius, rng);
|
||||
case SEA_STACK -> FormationShapeBuilder.seaStack(canvas, f, height, baseRadius, rng);
|
||||
case BOULDER -> FormationShapeBuilder.boulder(canvas, f, height, baseRadius, rng);
|
||||
case BASALT_COLUMN -> FormationShapeBuilder.basaltColumns(canvas, f, height, baseRadius, rng);
|
||||
case ICEBERG -> FormationShapeBuilder.iceberg(canvas, f, height, baseRadius, rng);
|
||||
case FISSURE -> FormationShapeBuilder.fissure(canvas, f, height, baseRadius, rng);
|
||||
case SPIRAL -> FormationShapeBuilder.spiral(canvas, f, height, baseRadius, rng);
|
||||
case OVERHANG -> FormationShapeBuilder.overhang(canvas, f, height, baseRadius, rng);
|
||||
}
|
||||
|
||||
if (canvas.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<Vector3i, BlockData> resolved = new HashMap<>();
|
||||
for (Map.Entry<Vector3i, FormationCanvas.Role> entry : canvas.getCells().entrySet()) {
|
||||
BlockData bd = FormationBlockResolver.resolve(f, blocks, entry.getValue(), entry.getKey());
|
||||
if (bd == null) {
|
||||
continue;
|
||||
}
|
||||
resolved.put(entry.getKey(), bd);
|
||||
}
|
||||
|
||||
return IrisProceduralBlocks.assemble(resolved);
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.formation;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("A width-shaping function evaluated over normalized height (0 at the base, 1 at the top). Controls how the formation radius scales between baseWidth at the bottom and topWidth at the top.")
|
||||
public enum FormationProfile {
|
||||
@Desc("Same radius at every height (a straight cylinder of constant baseWidth).")
|
||||
CONSTANT,
|
||||
@Desc("Radius ramps straight from baseWidth at the bottom to topWidth at the top.")
|
||||
LINEAR,
|
||||
@Desc("Radius shrinks smoothly toward a point as height increases (an ease that drives the SPIRE needle and gentle SEA_STACK taper).")
|
||||
TAPER,
|
||||
@Desc("Radius pinches to a narrow waist near profileWaist then widens again toward the top (the eroded waist used by HOODOO columns).")
|
||||
PARABOLIC,
|
||||
@Desc("Radius bulges out to its maximum in the middle of the height range and narrows at both ends (a barrel / bulged column).")
|
||||
BULGE
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.formation;
|
||||
|
||||
|
||||
|
||||
public final class FormationProfiles {
|
||||
private FormationProfiles() {
|
||||
}
|
||||
|
||||
public static double radiusAt(Formation f, double baseRadius, double topRadius, double t) {
|
||||
double clamped = Math.max(0.0, Math.min(1.0, t));
|
||||
return switch (f.profile()) {
|
||||
case CONSTANT -> baseRadius;
|
||||
case LINEAR -> baseRadius + (topRadius - baseRadius) * clamped;
|
||||
case TAPER -> baseRadius + (topRadius - baseRadius) * smoothstep(clamped);
|
||||
case PARABOLIC -> parabolic(f, baseRadius, topRadius, clamped);
|
||||
case BULGE -> baseRadius + (Math.max(baseRadius, topRadius) - baseRadius) * bell(clamped) + (topRadius - baseRadius) * clamped * 0.25;
|
||||
};
|
||||
}
|
||||
|
||||
private static double parabolic(Formation f, double baseRadius, double topRadius, double t) {
|
||||
double waist = Math.max(0.0, Math.min(1.0, f.profileWaist()));
|
||||
double floor = Math.max(0.0, Math.min(1.0, f.profileWaistFloor()));
|
||||
double denom = Math.pow(Math.max(waist, 1.0 - waist), 2);
|
||||
if (denom == 0) {
|
||||
denom = 1;
|
||||
}
|
||||
double envelope = baseRadius + (topRadius - baseRadius) * t;
|
||||
double pinch = floor + (1.0 - floor) * (Math.pow(t - waist, 2) / denom);
|
||||
return Math.max(0.5, envelope * pinch);
|
||||
}
|
||||
|
||||
private static double smoothstep(double t) {
|
||||
return 3.0 * t * t - 2.0 * t * t * t;
|
||||
}
|
||||
|
||||
private static double bell(double t) {
|
||||
return Math.exp(-Math.pow(t - 0.5, 2) / (2.0 * 0.22 * 0.22));
|
||||
}
|
||||
}
|
||||
+428
@@ -0,0 +1,428 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.formation;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.tree.TreeFunctions;
|
||||
import com.volmit.iris.util.math.Vector3i;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class FormationShapeBuilder {
|
||||
private FormationShapeBuilder() {
|
||||
}
|
||||
|
||||
public static void spire(FormationCanvas canvas, Formation f, int height, double baseRadius, RNG rng) {
|
||||
double topRadius = f.topWidth();
|
||||
column(canvas, f, height, baseRadius, topRadius, 0, 0, rng, true);
|
||||
double lean = Math.toRadians(f.lean());
|
||||
double azimuth = Math.toRadians(f.leanAzimuth());
|
||||
int previousX = 0;
|
||||
int previousZ = 0;
|
||||
for (int y = 0; y < height; y++) {
|
||||
double shear = Math.tan(lean) * y;
|
||||
int centerX = (int) Math.round(Math.cos(azimuth) * shear);
|
||||
int centerZ = (int) Math.round(Math.sin(azimuth) * shear);
|
||||
boolean cap = y >= height - 2;
|
||||
for (int x = Math.min(previousX, centerX); x <= Math.max(previousX, centerX); x++) {
|
||||
setSpireCenter(canvas, x, y, previousZ, cap);
|
||||
}
|
||||
for (int z = Math.min(previousZ, centerZ); z <= Math.max(previousZ, centerZ); z++) {
|
||||
setSpireCenter(canvas, centerX, y, z, cap);
|
||||
}
|
||||
previousX = centerX;
|
||||
previousZ = centerZ;
|
||||
}
|
||||
}
|
||||
|
||||
public static void seaStack(FormationCanvas canvas, Formation f, int height, double baseRadius, RNG rng) {
|
||||
double topRadius = Math.max(1.0, baseRadius * 0.55);
|
||||
if (f.topWidth() > 0) {
|
||||
topRadius = f.topWidth();
|
||||
}
|
||||
column(canvas, f, height, baseRadius, topRadius, 0, 0, rng, true);
|
||||
}
|
||||
|
||||
public static void hoodoo(FormationCanvas canvas, Formation f, int height, double baseRadius, RNG rng) {
|
||||
double topRadius = f.topWidth() > 0 ? f.topWidth() : Math.max(1.0, baseRadius * 0.7);
|
||||
column(canvas, f, height, baseRadius, topRadius, 0, 0, rng, false);
|
||||
|
||||
int capRadius = f.hoodooCapRadius();
|
||||
if (capRadius <= 0) {
|
||||
return;
|
||||
}
|
||||
int capHeight = Math.max(1, f.hoodooCapHeight());
|
||||
double lean = Math.toRadians(f.lean());
|
||||
double azimuth = Math.toRadians(f.leanAzimuth());
|
||||
double shear = Math.tan(lean) * height;
|
||||
double topOffX = Math.cos(azimuth) * shear;
|
||||
double topOffZ = Math.sin(azimuth) * shear;
|
||||
double wideRadius = topRadius + capRadius;
|
||||
|
||||
for (int cy = 0; cy < capHeight; cy++) {
|
||||
int y = height + cy;
|
||||
double shrink = capHeight <= 1 ? 0 : (cy / (double) (capHeight - 1)) * 0.5;
|
||||
double r = wideRadius * (1.0 - shrink);
|
||||
disc(canvas, f, (int) Math.round(topOffX), y, (int) Math.round(topOffZ), r, true, rng);
|
||||
}
|
||||
}
|
||||
|
||||
public static void boulder(FormationCanvas canvas, Formation f, int height, double baseRadius, RNG rng) {
|
||||
double rx = baseRadius;
|
||||
double ry = Math.max(2.0, height * 0.5);
|
||||
double rz = baseRadius * (0.8 + rng.d(0.0, 0.3));
|
||||
double roughness = Math.max(0.0, Math.min(1.0, f.roughness()));
|
||||
long noiseSeed = f.seed() + 4201L;
|
||||
|
||||
int maxR = (int) Math.ceil(Math.max(rx, Math.max(ry, rz))) + 2;
|
||||
for (int x = -maxR; x <= maxR; x++) {
|
||||
for (int y = 0; y <= (int) Math.ceil(ry * 2) + 1; y++) {
|
||||
for (int z = -maxR; z <= maxR; z++) {
|
||||
double nx = x / rx;
|
||||
double ny = (y - ry) / ry;
|
||||
double nz = z / rz;
|
||||
double d = nx * nx + ny * ny + nz * nz;
|
||||
double wobble = (TreeFunctions.valueNoise3D(x, y, z, noiseSeed) - 0.5) * roughness * 0.9;
|
||||
if (d + wobble <= 1.0) {
|
||||
canvas.setBody(x, y, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void arch(FormationCanvas canvas, Formation f, int height, double baseRadius, RNG rng) {
|
||||
double span = Math.max(2.0, f.archSpan());
|
||||
double tubeRadius = Math.max(0.75, f.archThickness() * 0.5);
|
||||
double asymmetry = Math.max(0.0, Math.min(1.0, f.archAsymmetry()));
|
||||
double leftFootX = -span * 0.5 - tubeRadius - rng.d(0.0, span * 0.16) * asymmetry;
|
||||
double rightFootX = span * 0.5 + tubeRadius + rng.d(0.0, span * 0.16) * asymmetry;
|
||||
double leftFootZ = rng.d(-tubeRadius * 0.45, tubeRadius * 0.45) * asymmetry;
|
||||
double rightFootZ = rng.d(-tubeRadius * 0.45, tubeRadius * 0.45) * asymmetry;
|
||||
double crownZ = rng.d(-span * 0.2, span * 0.2) * asymmetry;
|
||||
double crownShiftX = rng.d(-span * 0.12, span * 0.12) * asymmetry;
|
||||
double depthTwist = rng.d(-span * 0.1, span * 0.1) * asymmetry;
|
||||
double crownPosition = 0.5 + rng.d(-0.12, 0.12) * asymmetry;
|
||||
double leftSteepness = 0.58 + rng.d(-0.16, 0.16) * asymmetry;
|
||||
double rightSteepness = 0.58 + rng.d(-0.16, 0.16) * asymmetry;
|
||||
double radiusWave = rng.d(-0.22, 0.22) * asymmetry;
|
||||
double radiusPhase = rng.d(0.0, Math.PI * 2.0);
|
||||
double crownCenterY = Math.max(tubeRadius + 2.0, height - 1.0 - tubeRadius);
|
||||
double footRadius = tubeRadius + Math.min(tubeRadius * 0.45, baseRadius * 0.18);
|
||||
long noiseSeed = rng.nextLong();
|
||||
int steps = Math.max(24, (int) Math.ceil((rightFootX - leftFootX + height) * 4.0));
|
||||
|
||||
ball(canvas, f, (int) Math.round(leftFootX), (int) Math.round(footRadius),
|
||||
(int) Math.round(leftFootZ), footRadius, false, noiseSeed, rng);
|
||||
ball(canvas, f, (int) Math.round(rightFootX), (int) Math.round(footRadius),
|
||||
(int) Math.round(rightFootZ), footRadius, false, noiseSeed, rng);
|
||||
|
||||
for (int step = 0; step <= steps; step++) {
|
||||
double t = step / (double) steps;
|
||||
double normalizedRise;
|
||||
double steepness;
|
||||
if (t <= crownPosition) {
|
||||
normalizedRise = Math.sin((t / crownPosition) * Math.PI * 0.5);
|
||||
steepness = leftSteepness;
|
||||
} else {
|
||||
normalizedRise = Math.sin(((1.0 - t) / (1.0 - crownPosition)) * Math.PI * 0.5);
|
||||
steepness = rightSteepness;
|
||||
}
|
||||
double rise = Math.pow(Math.max(0.0, normalizedRise), steepness);
|
||||
double x = leftFootX + (rightFootX - leftFootX) * t
|
||||
+ Math.sin(Math.PI * t) * crownShiftX;
|
||||
double inverse = 1.0 - t;
|
||||
double z = inverse * inverse * leftFootZ + 2.0 * inverse * t * crownZ + t * t * rightFootZ
|
||||
+ Math.sin(Math.PI * 2.0 * t) * depthTwist;
|
||||
double y = tubeRadius + (crownCenterY - tubeRadius) * rise;
|
||||
double radius = tubeRadius * (1.0 + radiusWave * Math.sin(Math.PI * t)
|
||||
* Math.sin(Math.PI * 2.0 * t + radiusPhase));
|
||||
radius = Math.max(tubeRadius * 0.68, Math.min(tubeRadius * 1.32, radius));
|
||||
ball(canvas, f, (int) Math.round(x), (int) Math.round(y), (int) Math.round(z),
|
||||
radius, false, noiseSeed, rng);
|
||||
}
|
||||
if (asymmetry == 0.0) {
|
||||
mirrorAcrossX(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
public static void basaltColumns(FormationCanvas canvas, Formation f, int height, double baseRadius, RNG rng) {
|
||||
int count = Math.max(2, f.basaltColumns());
|
||||
int colRadius = Math.max(1, f.basaltColumnRadius());
|
||||
double variance = Math.max(0.0, Math.min(1.0, f.basaltHeightVariance()));
|
||||
int spread = (int) Math.ceil(baseRadius);
|
||||
|
||||
for (int c = 0; c < count; c++) {
|
||||
double angle = rng.d(0.0, Math.PI * 2.0);
|
||||
double dist = rng.d(0.0, spread);
|
||||
int ox = (int) Math.round(Math.cos(angle) * dist);
|
||||
int oz = (int) Math.round(Math.sin(angle) * dist);
|
||||
double hVar = 1.0 - variance + rng.d(0.0, variance * 2.0);
|
||||
int colHeight = Math.max(3, (int) Math.round(height * hVar));
|
||||
|
||||
for (int y = 0; y < colHeight; y++) {
|
||||
for (int x = -colRadius; x <= colRadius; x++) {
|
||||
for (int z = -colRadius; z <= colRadius; z++) {
|
||||
if (Math.abs(x) + Math.abs(z) > colRadius) {
|
||||
continue;
|
||||
}
|
||||
canvas.setBody(ox + x, y, oz + z);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (colRadius > 0) {
|
||||
canvas.setCap(ox, colHeight - 1, oz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void iceberg(FormationCanvas canvas, Formation f, int height, double baseRadius, RNG rng) {
|
||||
long noiseSeed = rng.nextLong();
|
||||
double bodyHeight = Math.max(2.0, height * 0.42);
|
||||
double radiusX = baseRadius * rng.d(1.15, 1.5);
|
||||
double radiusZ = baseRadius * rng.d(1.0, 1.35);
|
||||
ellipsoid(canvas, radiusX, bodyHeight, radiusZ, bodyHeight * 0.36, noiseSeed, f.roughness());
|
||||
|
||||
int peaks = Math.max(1, f.icebergPeaks());
|
||||
double phase = rng.d(0.0, Math.PI * 2.0);
|
||||
for (int i = 0; i < peaks; i++) {
|
||||
double angle = phase + (Math.PI * 2.0 * i / peaks) + rng.d(-0.35, 0.35);
|
||||
double distance = rng.d(0.0, baseRadius * 0.62);
|
||||
int offsetX = (int) Math.round(Math.cos(angle) * distance);
|
||||
int offsetZ = (int) Math.round(Math.sin(angle) * distance);
|
||||
int peakHeight = Math.max(3, (int) Math.round(height * rng.d(0.58, 1.0)));
|
||||
double peakRadius = Math.max(1.0, baseRadius * rng.d(0.28, 0.58));
|
||||
double leanDistance = rng.d(0.0, Math.max(1.0, baseRadius * 0.45));
|
||||
taperedPeak(canvas, f, peakHeight, peakRadius, offsetX, offsetZ, angle, leanDistance, rng);
|
||||
}
|
||||
}
|
||||
|
||||
public static void fissure(FormationCanvas canvas, Formation f, int height, double baseRadius, RNG rng) {
|
||||
int count = Math.max(2, f.fractureCount());
|
||||
double separation = Math.max(1.0, f.fractureSeparation());
|
||||
double shardRadius = Math.max(1.0, baseRadius / Math.max(1.6, count * 0.72));
|
||||
double phase = rng.d(0.0, Math.PI * 2.0);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
double lane = i - ((count - 1) / 2.0);
|
||||
double offset = lane * (shardRadius * 2.0 + separation);
|
||||
int offsetX = (int) Math.round(Math.cos(phase) * offset);
|
||||
int offsetZ = (int) Math.round(Math.sin(phase) * offset);
|
||||
double outwardAngle = lane < 0 ? phase + Math.PI : phase;
|
||||
int shardHeight = Math.max(3, (int) Math.round(height * rng.d(0.62, 1.0)));
|
||||
double leanDistance = rng.d(separation * 0.4, separation + baseRadius * 0.8);
|
||||
taperedPeak(canvas, f, shardHeight, shardRadius, offsetX, offsetZ, outwardAngle, leanDistance, rng);
|
||||
}
|
||||
}
|
||||
|
||||
public static void spiral(FormationCanvas canvas, Formation f, int height, double baseRadius, RNG rng) {
|
||||
int steps = Math.max(12, height * 4);
|
||||
double turns = Math.max(0.25, f.spiralTurns());
|
||||
double startRadius = Math.max(1.0, f.spiralRadius());
|
||||
double startThickness = Math.max(1.0, f.spiralThickness());
|
||||
double phase = rng.d(0.0, Math.PI * 2.0);
|
||||
long noiseSeed = rng.nextLong();
|
||||
|
||||
for (int step = 0; step <= steps; step++) {
|
||||
double t = step / (double) steps;
|
||||
double angle = phase + Math.PI * 2.0 * turns * t;
|
||||
double radius = startRadius * (1.0 - 0.72 * t);
|
||||
int x = (int) Math.round(Math.cos(angle) * radius);
|
||||
int y = (int) Math.round(t * (height - 1));
|
||||
int z = (int) Math.round(Math.sin(angle) * radius);
|
||||
double thickness = Math.max(0.75, startThickness * (1.0 - 0.48 * t));
|
||||
ball(canvas, f, x, y, z, thickness, step == steps, noiseSeed, rng);
|
||||
}
|
||||
|
||||
double footRadius = Math.max(1.0, Math.min(baseRadius, startThickness + 1.0));
|
||||
disc(canvas, f, (int) Math.round(Math.cos(phase) * startRadius), 0,
|
||||
(int) Math.round(Math.sin(phase) * startRadius), footRadius, false, rng);
|
||||
}
|
||||
|
||||
public static void overhang(FormationCanvas canvas, Formation f, int height, double baseRadius, RNG rng) {
|
||||
int steps = Math.max(12, height * 4);
|
||||
double reach = Math.max(1.0, f.overhangReach());
|
||||
double drop = Math.max(0.0, f.overhangDrop());
|
||||
double phase = Math.toRadians(f.leanAzimuth()) + rng.d(-0.18, 0.18);
|
||||
double trunkHeight = Math.max(3.0, height * 0.62);
|
||||
long noiseSeed = rng.nextLong();
|
||||
|
||||
for (int step = 0; step <= steps; step++) {
|
||||
double t = step / (double) steps;
|
||||
double extension;
|
||||
double y;
|
||||
if (t <= 0.58) {
|
||||
double rise = t / 0.58;
|
||||
extension = reach * 0.16 * rise * rise;
|
||||
y = trunkHeight * rise;
|
||||
} else {
|
||||
double hook = (t - 0.58) / 0.42;
|
||||
extension = reach * (0.16 + 0.84 * Math.sin(hook * Math.PI * 0.5));
|
||||
y = trunkHeight + Math.sin(hook * Math.PI) * height * 0.18 - drop * hook * hook;
|
||||
}
|
||||
int x = (int) Math.round(Math.cos(phase) * extension);
|
||||
int z = (int) Math.round(Math.sin(phase) * extension);
|
||||
double thickness = Math.max(0.9, baseRadius * (1.0 - 0.64 * t));
|
||||
ball(canvas, f, x, Math.max(0, (int) Math.round(y)), z, thickness, step == steps, noiseSeed, rng);
|
||||
}
|
||||
}
|
||||
|
||||
private static void column(FormationCanvas canvas, Formation f, int height, double baseRadius, double topRadius, int extraBaseX, int extraBaseZ, RNG rng, boolean capTop) {
|
||||
double lean = Math.toRadians(f.lean());
|
||||
double azimuth = Math.toRadians(f.leanAzimuth());
|
||||
double roughness = Math.max(0.0, Math.min(1.0, f.roughness()));
|
||||
double jitter = Math.max(0.0, Math.min(1.0, f.jitter()));
|
||||
long noiseSeed = f.seed() + 9001L;
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
double t = height <= 1 ? 1.0 : (y / (double) (height - 1));
|
||||
double radius = FormationProfiles.radiusAt(f, baseRadius, topRadius, t);
|
||||
double shear = Math.tan(lean) * y;
|
||||
int cx = extraBaseX + (int) Math.round(Math.cos(azimuth) * shear);
|
||||
int cz = extraBaseZ + (int) Math.round(Math.sin(azimuth) * shear);
|
||||
boolean cap = capTop && y >= height - 2;
|
||||
ringDisc(canvas, f, cx, y, cz, radius, roughness, jitter, noiseSeed, cap, rng);
|
||||
}
|
||||
}
|
||||
|
||||
private static void setSpireCenter(FormationCanvas canvas, int x, int y, int z, boolean cap) {
|
||||
if (cap) {
|
||||
canvas.setCap(x, y, z);
|
||||
} else {
|
||||
canvas.setBody(x, y, z);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ringDisc(FormationCanvas canvas, Formation f, int cx, int y, int cz, double radius, double roughness, double jitter, long noiseSeed, boolean cap, RNG rng) {
|
||||
int r = (int) Math.ceil(radius) + 1;
|
||||
for (int x = -r; x <= r; x++) {
|
||||
for (int z = -r; z <= r; z++) {
|
||||
double dist = Math.sqrt(x * x + z * z);
|
||||
double perturb = (TreeFunctions.valueNoise3D(cx + x, y, cz + z, noiseSeed) - 0.5) * roughness * (radius + 1.0);
|
||||
double effective = radius + perturb;
|
||||
if (dist <= effective) {
|
||||
if (jitter > 0 && dist > effective - 1.0 && rng.chance(jitter * 0.5)) {
|
||||
continue;
|
||||
}
|
||||
if (cap) {
|
||||
canvas.setCap(cx + x, y, cz + z);
|
||||
} else {
|
||||
canvas.setBody(cx + x, y, cz + z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void disc(FormationCanvas canvas, Formation f, int cx, int y, int cz, double radius, boolean cap, RNG rng) {
|
||||
int r = (int) Math.ceil(radius) + 1;
|
||||
for (int x = -r; x <= r; x++) {
|
||||
for (int z = -r; z <= r; z++) {
|
||||
if (Math.sqrt(x * x + z * z) <= radius) {
|
||||
if (cap) {
|
||||
canvas.setCap(cx + x, y, cz + z);
|
||||
} else {
|
||||
canvas.setBody(cx + x, y, cz + z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void taperedPeak(FormationCanvas canvas, Formation f, int height, double radius,
|
||||
int offsetX, int offsetZ, double leanAngle, double leanDistance, RNG rng) {
|
||||
long noiseSeed = rng.nextLong();
|
||||
double roughness = Math.max(0.0, Math.min(1.0, f.roughness()));
|
||||
double jitter = Math.max(0.0, Math.min(1.0, f.jitter()));
|
||||
for (int y = 0; y < height; y++) {
|
||||
double t = height <= 1 ? 1.0 : y / (double) (height - 1);
|
||||
int x = offsetX + (int) Math.round(Math.cos(leanAngle) * leanDistance * t * t);
|
||||
int z = offsetZ + (int) Math.round(Math.sin(leanAngle) * leanDistance * t * t);
|
||||
double layerRadius = Math.max(0.45, radius * Math.pow(1.0 - t, 0.72));
|
||||
ringDisc(canvas, f, x, y, z, layerRadius, roughness, jitter, noiseSeed, y >= height - 2, rng);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ellipsoid(FormationCanvas canvas, double radiusX, double radiusY, double radiusZ,
|
||||
double centerY, long noiseSeed, double configuredRoughness) {
|
||||
double roughness = Math.max(0.0, Math.min(1.0, configuredRoughness));
|
||||
int maxX = (int) Math.ceil(radiusX) + 1;
|
||||
int maxY = (int) Math.ceil(radiusY) + 1;
|
||||
int maxZ = (int) Math.ceil(radiusZ) + 1;
|
||||
for (int x = -maxX; x <= maxX; x++) {
|
||||
for (int y = 0; y <= maxY; y++) {
|
||||
for (int z = -maxZ; z <= maxZ; z++) {
|
||||
double nx = x / radiusX;
|
||||
double ny = (y - centerY) / radiusY;
|
||||
double nz = z / radiusZ;
|
||||
double distance = nx * nx + ny * ny + nz * nz;
|
||||
double wobble = (TreeFunctions.valueNoise3D(x, y, z, noiseSeed) - 0.5) * roughness * 0.55;
|
||||
if (distance + wobble <= 1.0) {
|
||||
canvas.setBody(x, y, z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ball(FormationCanvas canvas, Formation f, int centerX, int centerY, int centerZ,
|
||||
double radius, boolean cap, long noiseSeed, RNG rng) {
|
||||
int extent = (int) Math.ceil(radius);
|
||||
double roughness = Math.max(0.0, Math.min(1.0, f.roughness()));
|
||||
double jitter = Math.max(0.0, Math.min(1.0, f.jitter()));
|
||||
for (int x = -extent; x <= extent; x++) {
|
||||
for (int y = -extent; y <= extent; y++) {
|
||||
for (int z = -extent; z <= extent; z++) {
|
||||
double distance = Math.sqrt(x * x + y * y + z * z);
|
||||
double perturb = (TreeFunctions.valueNoise3D(centerX + x, centerY + y, centerZ + z, noiseSeed) - 0.5)
|
||||
* roughness * Math.max(1.0, radius * 0.6);
|
||||
double effectiveRadius = radius + perturb;
|
||||
if (distance > effectiveRadius + 0.2) {
|
||||
continue;
|
||||
}
|
||||
if (jitter > 0.0 && distance > effectiveRadius - 0.75 && rng.chance(jitter * 0.35)) {
|
||||
continue;
|
||||
}
|
||||
int targetY = centerY + y;
|
||||
if (targetY < 0) {
|
||||
continue;
|
||||
}
|
||||
if (cap) {
|
||||
canvas.setCap(centerX + x, targetY, centerZ + z);
|
||||
} else {
|
||||
canvas.setBody(centerX + x, targetY, centerZ + z);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void mirrorAcrossX(FormationCanvas canvas) {
|
||||
Map<Vector3i, FormationCanvas.Role> cells = new HashMap<>(canvas.getCells());
|
||||
for (Map.Entry<Vector3i, FormationCanvas.Role> entry : cells.entrySet()) {
|
||||
Vector3i position = entry.getKey();
|
||||
if (entry.getValue() == FormationCanvas.Role.CAP) {
|
||||
canvas.setCap(-position.getBlockX(), position.getBlockY(), position.getBlockZ());
|
||||
} else {
|
||||
canvas.setBody(-position.getBlockX(), position.getBlockY(), position.getBlockZ());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.formation;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBuilder;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralShape;
|
||||
import com.volmit.iris.engine.object.procedural.ResolvedProceduralPlacement;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
/** A rock formation made placeable: its variants are baked by {@link FormationGenerator}. */
|
||||
public final class ResolvedFormation extends ResolvedProceduralPlacement {
|
||||
private final Formation settings;
|
||||
|
||||
public ResolvedFormation(Formation settings, IrisData data) {
|
||||
super(new Shape("formation", settings.name(), settings.chance(), settings.density(), settings.seed(), settings.variants(), settings.mode(),
|
||||
settings.rotation(), settings.clamp(), settings.carvingSupport(), settings.underwater(), settings.translate(),
|
||||
settings.stiltSettings().orElse(null)), data);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected ProceduralShape generate(int index, RNG rng, ObjectBuilder blocks) {
|
||||
return FormationGenerator.generate(settings, index, rng, blocks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,347 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.fungi;
|
||||
|
||||
import com.volmit.iris.engine.object.ObjectPlaceMode;
|
||||
import com.volmit.iris.engine.object.CarvingMode;
|
||||
import com.volmit.iris.engine.object.IrisObjectRotation;
|
||||
import com.volmit.iris.engine.object.IrisObjectTranslate;
|
||||
import com.volmit.iris.engine.object.IrisStiltSettings;
|
||||
import com.volmit.iris.engine.object.IrisObjectLimit;
|
||||
import com.volmit.iris.engine.object.IrisMaterialPalette;
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
import com.volmit.iris.engine.object.annotations.Snippet;
|
||||
import com.volmit.iris.engine.object.annotations.MaxNumber;
|
||||
import com.volmit.iris.engine.object.annotations.MinNumber;
|
||||
import com.volmit.iris.engine.object.annotations.RegistryListBlockType;
|
||||
import com.volmit.iris.engine.object.annotations.Required;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Snippet("fungus")
|
||||
@Desc("A single procedurally generated fungus (giant mushroom or shelf fungus). Iris bakes a pool of deterministic variants from these settings and scatters them at world-gen time, exactly like an object placement but generated from scratch instead of loaded from an iob file. The fungus is built as a stem column with a cap dome grown on top, mirroring the trunk-and-canopy model used by procedural trees.")
|
||||
public final class Fungus {
|
||||
@Desc("A human readable name used in logs and as the variant load key.")
|
||||
private String name = "fungus";
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The chance per chunk for this fungus to attempt placement. Use density for multiple per chunk.")
|
||||
private double chance = 0.4;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("If the chance check passes, attempt this many placements in the chunk.")
|
||||
private int density = 1;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("How many distinct variants to pre-bake for this fungus. Higher means more variety at a small memory cost.")
|
||||
private int variants = 6;
|
||||
|
||||
@Desc("The base seed for deterministic generation. The same seed and settings always bake the same variants.")
|
||||
private long seed = 1337;
|
||||
|
||||
@Desc("The placement mode used to anchor the fungus to the terrain.")
|
||||
private ObjectPlaceMode mode = ObjectPlaceMode.CENTER_HEIGHT;
|
||||
|
||||
@Desc("Rotate this fungus's placement.")
|
||||
private IrisObjectRotation rotation = new IrisObjectRotation();
|
||||
|
||||
@Desc("Limit the max or min height of placement.")
|
||||
private IrisObjectLimit clamp = new IrisObjectLimit();
|
||||
|
||||
@Desc("Whether this fungus may place on the terrain surface, under carvings, or both.")
|
||||
private CarvingMode carvingSupport = CarvingMode.SURFACE_ONLY;
|
||||
|
||||
@Desc("If true, the fungus anchors on the terrain height ignoring the water surface.")
|
||||
private boolean underwater = false;
|
||||
|
||||
@Desc("Translate (offset) this placement along each axis; for example set a negative y to sink it into the ground.")
|
||||
private IrisObjectTranslate translate = new IrisObjectTranslate();
|
||||
|
||||
@Desc("Settings for the stilt place modes (STILT, MIN_STILT, FAST_STILT, CENTER_STILT, ERODE_STILT, ORGANIC_STILT).")
|
||||
private IrisStiltSettings stiltSettings;
|
||||
|
||||
@Required
|
||||
@Desc("The stem block, e.g. minecraft:mushroom_stem. Ignored when stemPalette is set.")
|
||||
@RegistryListBlockType
|
||||
private String stem = "minecraft:mushroom_stem";
|
||||
|
||||
@Desc("A noise-driven palette for the stem. When set this overrides the single stem block, letting the stem mix blocks by noise.")
|
||||
private IrisMaterialPalette stemPalette;
|
||||
|
||||
@Required
|
||||
@Desc("The cap block, e.g. minecraft:red_mushroom_block. Ignored when capPalette is set.")
|
||||
@RegistryListBlockType
|
||||
private String cap = "minecraft:red_mushroom_block";
|
||||
|
||||
@Desc("A noise-driven palette for the cap. When set this overrides the single cap block, letting the cap mix blocks by noise.")
|
||||
private IrisMaterialPalette capPalette;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Minimum total stem height in blocks. Variants spread between this and stemHeightMax.")
|
||||
private int stemHeightMin = 5;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Maximum total stem height in blocks. Variants spread between stemHeightMin and this.")
|
||||
private int stemHeightMax = 9;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(3)
|
||||
@Desc("Base stem width in blocks. 1 is a single column, 2 is a 2x2 stem, 3 is a chunky 3x3 trunk.")
|
||||
private int stemWidth = 1;
|
||||
|
||||
@Desc("How far the stem leans away from vertical, in degrees. 0 is perfectly upright; small values give an organic tilt.")
|
||||
private double stemCurve = 0;
|
||||
|
||||
@Desc("Compass direction in degrees the stem leans toward when stemCurve is non-zero.")
|
||||
private double stemLeanAzimuth = 0;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Amplitude in blocks of a gentle sideways wave applied up the stem so it is not a perfectly straight ruler.")
|
||||
private double stemWaveAmplitude = 0.4;
|
||||
|
||||
@MinNumber(0.0001)
|
||||
@Desc("The number of full sine wobbles applied over the stem height for the stem wave.")
|
||||
private double stemWavePeriods = 1;
|
||||
|
||||
@Desc("The silhouette of the cap (dome, flat umbrella, funnel, cone, or wide shallow slab).")
|
||||
private FungusCapShape capShape = FungusCapShape.DOME;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Minimum cap radius in blocks (measured from the cap center to its rim).")
|
||||
private int capRadiusMin = 3;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Maximum cap radius in blocks (measured from the cap center to its rim).")
|
||||
private int capRadiusMax = 5;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(3)
|
||||
@Desc("How many blocks thick the cap shell is. 1 is a thin skin, 3 is a fleshy slab.")
|
||||
private int capThickness = 1;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Vertical flatten factor for the cap. 0 leaves the cap at full height, 1 squashes it flat into a disc.")
|
||||
private double capSquish = 0.4;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("How far the cap rim droops downward, in degrees, curling the edge of the cap toward the ground.")
|
||||
private double capDroop = 20;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("How far the cap radius extends past the stem before the rim begins, in blocks. Larger values make the cap overhang the stem more.")
|
||||
private double capOverhang = 2;
|
||||
|
||||
@Desc("Optional block forming the gill layer on the underside of the cap (gills or a glow layer), e.g. minecraft:brown_mushroom_block or minecraft:shroomlight. Ignored when gillPalette is set or left null.")
|
||||
@RegistryListBlockType
|
||||
private String gillBlock = null;
|
||||
|
||||
@Desc("A noise-driven palette for the underside gill layer. When set this overrides the single gillBlock.")
|
||||
private IrisMaterialPalette gillPalette;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The fraction of underside cap blocks replaced by the gill block when a gill block or palette is set.")
|
||||
private double gillChance = 0.85;
|
||||
|
||||
@Desc("Optional block speckled across the top of the cap by noise (white toadstool dots, warts, glowing spots), e.g. minecraft:bone_block or minecraft:white_concrete. Ignored when spotPalette is set or left null.")
|
||||
@RegistryListBlockType
|
||||
private String spotBlock = null;
|
||||
|
||||
@Desc("A noise-driven palette for the cap top spots. When set this overrides the single spotBlock.")
|
||||
private IrisMaterialPalette spotPalette;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The fraction of top cap blocks replaced by the spot block, selected by value noise so spots cluster naturally.")
|
||||
private double spotChance = 0.18;
|
||||
|
||||
@Desc("If true, generate a bracket / shelf fungus instead of an upright mushroom: a small flat fan that grows sideways off a very short or absent stem, like a polypore clinging to a trunk.")
|
||||
private boolean shelf = false;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("The radius in blocks of the sideways fan when shelf is true.")
|
||||
private int shelfRadius = 3;
|
||||
|
||||
public Fungus() {
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public double chance() {
|
||||
return chance;
|
||||
}
|
||||
|
||||
public int density() {
|
||||
return density;
|
||||
}
|
||||
|
||||
public int variants() {
|
||||
return variants;
|
||||
}
|
||||
|
||||
public long seed() {
|
||||
return seed;
|
||||
}
|
||||
|
||||
public ObjectPlaceMode mode() {
|
||||
return mode == null ? ObjectPlaceMode.CENTER_HEIGHT : mode;
|
||||
}
|
||||
|
||||
public IrisObjectRotation rotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public IrisObjectLimit clamp() {
|
||||
return clamp;
|
||||
}
|
||||
|
||||
public CarvingMode carvingSupport() {
|
||||
return carvingSupport == null ? CarvingMode.SURFACE_ONLY : carvingSupport;
|
||||
}
|
||||
|
||||
public boolean underwater() {
|
||||
return underwater;
|
||||
}
|
||||
|
||||
public IrisObjectTranslate translate() {
|
||||
return translate;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisStiltSettings> stiltSettings() {
|
||||
return Optional.ofNullable(stiltSettings);
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
|
||||
public String stem() {
|
||||
return stem;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisMaterialPalette> stemPalette() {
|
||||
return Optional.ofNullable(stemPalette);
|
||||
}
|
||||
|
||||
public String cap() {
|
||||
return cap;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisMaterialPalette> capPalette() {
|
||||
return Optional.ofNullable(capPalette);
|
||||
}
|
||||
|
||||
public int stemHeightMin() {
|
||||
return stemHeightMin;
|
||||
}
|
||||
|
||||
public int stemHeightMax() {
|
||||
return stemHeightMax;
|
||||
}
|
||||
|
||||
public int stemWidth() {
|
||||
return stemWidth;
|
||||
}
|
||||
|
||||
public double stemCurve() {
|
||||
return stemCurve;
|
||||
}
|
||||
|
||||
public double stemLeanAzimuth() {
|
||||
return stemLeanAzimuth;
|
||||
}
|
||||
|
||||
public double stemWaveAmplitude() {
|
||||
return stemWaveAmplitude;
|
||||
}
|
||||
|
||||
public double stemWavePeriods() {
|
||||
return stemWavePeriods;
|
||||
}
|
||||
|
||||
public FungusCapShape capShape() {
|
||||
return capShape == null ? FungusCapShape.DOME : capShape;
|
||||
}
|
||||
|
||||
public int capRadiusMin() {
|
||||
return capRadiusMin;
|
||||
}
|
||||
|
||||
public int capRadiusMax() {
|
||||
return capRadiusMax;
|
||||
}
|
||||
|
||||
public int capThickness() {
|
||||
return capThickness;
|
||||
}
|
||||
|
||||
public double capSquish() {
|
||||
return capSquish;
|
||||
}
|
||||
|
||||
public double capDroop() {
|
||||
return capDroop;
|
||||
}
|
||||
|
||||
public double capOverhang() {
|
||||
return capOverhang;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<String> gillBlock() {
|
||||
return Optional.ofNullable(gillBlock);
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisMaterialPalette> gillPalette() {
|
||||
return Optional.ofNullable(gillPalette);
|
||||
}
|
||||
|
||||
public double gillChance() {
|
||||
return gillChance;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<String> spotBlock() {
|
||||
return Optional.ofNullable(spotBlock);
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisMaterialPalette> spotPalette() {
|
||||
return Optional.ofNullable(spotPalette);
|
||||
}
|
||||
|
||||
public double spotChance() {
|
||||
return spotChance;
|
||||
}
|
||||
|
||||
public boolean shelf() {
|
||||
return shelf;
|
||||
}
|
||||
|
||||
public int shelfRadius() {
|
||||
return shelfRadius;
|
||||
}
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.fungi;
|
||||
|
||||
import com.volmit.iris.engine.object.IrisMaterialPalette;
|
||||
import java.util.Optional;
|
||||
import com.volmit.iris.engine.object.procedural.IrisProceduralBlocks;
|
||||
import com.volmit.iris.engine.object.procedural.tree.TreeFunctions;
|
||||
import com.volmit.iris.util.math.Vector3i;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public final class FungusCapBuilder {
|
||||
private FungusCapBuilder() {
|
||||
}
|
||||
|
||||
public static void build(Map<Vector3i, FungusCellRole> roles, Fungus fungus, int baseRadius, double cx, int baseY, double cz, long seed) {
|
||||
int radius = (int) Math.round(baseRadius + Math.max(0.0, fungus.capOverhang()));
|
||||
int thickness = Math.max(1, Math.min(3, fungus.capThickness()));
|
||||
double squish = Math.max(0.0, Math.min(1.0, fungus.capSquish()));
|
||||
double droop = Math.max(0.0, fungus.capDroop());
|
||||
FungusCapShape shape = fungus.capShape() == null ? FungusCapShape.DOME : fungus.capShape();
|
||||
int icx = (int) Math.round(cx);
|
||||
int icz = (int) Math.round(cz);
|
||||
|
||||
boolean hasGill = blockOrPaletteSet(fungus.gillBlock(), fungus.gillPalette());
|
||||
boolean hasSpot = blockOrPaletteSet(fungus.spotBlock(), fungus.spotPalette());
|
||||
|
||||
double apexRise = apexRise(shape, radius) * (1.0 - 0.6 * squish);
|
||||
double droopReach = Math.tan(Math.toRadians(droop)) * radius;
|
||||
|
||||
for (int dx = -radius; dx <= radius; dx++) {
|
||||
for (int dz = -radius; dz <= radius; dz++) {
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
if (dist > radius + 0.5) {
|
||||
continue;
|
||||
}
|
||||
double normalized = dist / Math.max(1.0, radius);
|
||||
double surfaceTop = baseY + capSurface(shape, normalized, apexRise);
|
||||
surfaceTop -= droopReach * Math.pow(normalized, 3);
|
||||
|
||||
int topYi = (int) Math.round(surfaceTop);
|
||||
for (int layer = 0; layer < thickness; layer++) {
|
||||
int y = topYi - layer;
|
||||
if (y < baseY - thickness) {
|
||||
continue;
|
||||
}
|
||||
Vector3i pos = new Vector3i(icx + dx, y, icz + dz);
|
||||
FungusCellRole role = FungusCellRole.CAP;
|
||||
|
||||
boolean underside = layer == thickness - 1;
|
||||
boolean topFace = layer == 0;
|
||||
|
||||
if (underside && hasGill && rollNoise(icx + dx, y, icz + dz, seed + 4111L) <= fungus.gillChance()) {
|
||||
role = FungusCellRole.GILL;
|
||||
} else if (topFace && hasSpot && spotNoise(icx + dx, y, icz + dz, seed + 9311L) <= fungus.spotChance()) {
|
||||
role = FungusCellRole.SPOT;
|
||||
}
|
||||
|
||||
roles.putIfAbsent(pos, role);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static double apexRise(FungusCapShape shape, int radius) {
|
||||
return switch (shape) {
|
||||
case DOME -> radius * 0.85;
|
||||
case CONICAL -> radius * 1.4;
|
||||
case FUNNEL -> radius * 0.5;
|
||||
case FLAT -> radius * 0.35;
|
||||
case FLAT_WIDE -> radius * 0.2;
|
||||
};
|
||||
}
|
||||
|
||||
private static double capSurface(FungusCapShape shape, double normalized, double apexRise) {
|
||||
double n = Math.max(0.0, Math.min(1.0, normalized));
|
||||
return switch (shape) {
|
||||
case DOME -> apexRise * Math.sqrt(Math.max(0.0, 1.0 - n * n));
|
||||
case CONICAL -> apexRise * (1.0 - n);
|
||||
case FUNNEL -> apexRise * (n * n);
|
||||
case FLAT -> apexRise * Math.max(0.0, 1.0 - Math.pow(n, 4));
|
||||
case FLAT_WIDE -> apexRise * Math.max(0.0, 1.0 - Math.pow(n, 6));
|
||||
};
|
||||
}
|
||||
|
||||
private static double rollNoise(int x, int y, int z, long seed) {
|
||||
return TreeFunctions.valueNoise3D(x, y, z, seed);
|
||||
}
|
||||
|
||||
private static double spotNoise(int x, int y, int z, long seed) {
|
||||
return TreeFunctions.valueNoise3D(Math.floorDiv(x, 2), y, Math.floorDiv(z, 2), seed);
|
||||
}
|
||||
|
||||
static boolean blockOrPaletteSet(Optional<String> block, Optional<IrisMaterialPalette> palette) {
|
||||
if (IrisProceduralBlocks.paletteSet(palette)) {
|
||||
return true;
|
||||
}
|
||||
return block.filter(name -> !name.isEmpty()).isPresent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.fungi;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("The silhouette of the mushroom cap. Each shape drives how the cap radius and thickness vary from its apex down to the rim, giving the fungus a distinct profile.")
|
||||
public enum FungusCapShape {
|
||||
@Desc("A rounded hemisphere cap that bulges up and curls slightly under at the rim, the classic red toadstool dome.")
|
||||
DOME,
|
||||
@Desc("A wide, nearly flat umbrella that holds its full radius almost to the edge before dropping, the brown-mushroom plate look.")
|
||||
FLAT,
|
||||
@Desc("An upturned bowl whose rim lifts higher than its center, cupping upward like a chanterelle or funnel mushroom.")
|
||||
FUNNEL,
|
||||
@Desc("A tall pointed cone that tapers from a broad base to a sharp apex, the witch-hat parasol silhouette.")
|
||||
CONICAL,
|
||||
@Desc("A very wide but shallow slab, a low overhanging shelf-like roof that spreads far past the stem with minimal vertical rise.")
|
||||
FLAT_WIDE
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.fungi;
|
||||
|
||||
enum FungusCellRole {
|
||||
STEM,
|
||||
CAP,
|
||||
GILL,
|
||||
SPOT
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.fungi;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBuilder;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralShape;
|
||||
import com.volmit.iris.engine.object.procedural.IrisProceduralBlocks;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import com.volmit.iris.util.math.Vector3i;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public final class FungusGenerator {
|
||||
private FungusGenerator() {
|
||||
}
|
||||
|
||||
public static ProceduralShape generate(Fungus fungus, int height, RNG rng, ObjectBuilder blocks) {
|
||||
Map<Vector3i, FungusCellRole> roles = new HashMap<>();
|
||||
long baseSeed = rng.getSeed();
|
||||
|
||||
if (fungus.shelf()) {
|
||||
FungusShelfBuilder.build(roles, fungus, rng, baseSeed);
|
||||
} else {
|
||||
buildUpright(roles, fungus, height, rng, baseSeed);
|
||||
}
|
||||
|
||||
if (roles.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
RNG paletteRng = new RNG(fungus.seed());
|
||||
Map<Vector3i, BlockData> resolved = new HashMap<>();
|
||||
for (Map.Entry<Vector3i, FungusCellRole> entry : roles.entrySet()) {
|
||||
Vector3i pos = entry.getKey();
|
||||
BlockData bd = resolveRole(fungus, entry.getValue(), blocks, pos, paletteRng);
|
||||
if (bd == null) {
|
||||
continue;
|
||||
}
|
||||
resolved.put(pos, bd);
|
||||
}
|
||||
|
||||
return IrisProceduralBlocks.assemble(resolved);
|
||||
}
|
||||
|
||||
private static void buildUpright(Map<Vector3i, FungusCellRole> roles, Fungus fungus, int height, RNG rng, long baseSeed) {
|
||||
int stemHeight = Math.max(1, height);
|
||||
Map<Vector3i, BlockData> stemCells = new HashMap<>();
|
||||
double[] top = FungusStemBuilder.build(stemCells, fungus, stemHeight, baseSeed);
|
||||
for (Vector3i v : stemCells.keySet()) {
|
||||
roles.put(v, FungusCellRole.STEM);
|
||||
}
|
||||
|
||||
int radius = pickRadius(fungus, rng);
|
||||
double cx = top[0];
|
||||
double cz = top[2];
|
||||
int capBaseY = (int) Math.round(top[1]);
|
||||
FungusCapBuilder.build(roles, fungus, radius, cx, capBaseY, cz, baseSeed + 5557L);
|
||||
}
|
||||
|
||||
private static int pickRadius(Fungus fungus, RNG rng) {
|
||||
int lo = Math.min(fungus.capRadiusMin(), fungus.capRadiusMax());
|
||||
int hi = Math.max(fungus.capRadiusMin(), fungus.capRadiusMax());
|
||||
lo = Math.max(1, lo);
|
||||
hi = Math.max(lo, hi);
|
||||
return rng.i(lo, hi + 1);
|
||||
}
|
||||
|
||||
private static BlockData resolveRole(Fungus fungus, FungusCellRole role, ObjectBuilder blocks, Vector3i pos, RNG paletteRng) {
|
||||
int x = pos.getBlockX();
|
||||
int y = pos.getBlockY();
|
||||
int z = pos.getBlockZ();
|
||||
return switch (role) {
|
||||
case STEM -> IrisProceduralBlocks.resolve(fungus.stem(), fungus.stemPalette(), blocks, x, y, z, paletteRng);
|
||||
case CAP -> IrisProceduralBlocks.resolve(fungus.cap(), fungus.capPalette(), blocks, x, y, z, paletteRng);
|
||||
case GILL -> {
|
||||
BlockData gill = IrisProceduralBlocks.resolve(fungus.gillBlock(), fungus.gillPalette(), blocks, x, y, z, paletteRng);
|
||||
yield gill != null ? gill : IrisProceduralBlocks.resolve(fungus.cap(), fungus.capPalette(), blocks, x, y, z, paletteRng);
|
||||
}
|
||||
case SPOT -> {
|
||||
BlockData spot = IrisProceduralBlocks.resolve(fungus.spotBlock(), fungus.spotPalette(), blocks, x, y, z, paletteRng);
|
||||
yield spot != null ? spot : IrisProceduralBlocks.resolve(fungus.cap(), fungus.capPalette(), blocks, x, y, z, paletteRng);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.fungi;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.tree.TreeFunctions;
|
||||
import com.volmit.iris.util.math.Vector3i;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public final class FungusShelfBuilder {
|
||||
private FungusShelfBuilder() {
|
||||
}
|
||||
|
||||
public static void build(Map<Vector3i, FungusCellRole> roles, Fungus fungus, RNG rng, long seed) {
|
||||
int radius = Math.max(1, fungus.shelfRadius());
|
||||
double azimuth = rng.d(0.0, Math.PI * 2.0);
|
||||
double fanX = Math.sin(azimuth);
|
||||
double fanZ = Math.cos(azimuth);
|
||||
int stubHeight = Math.max(0, Math.min(2, fungus.stemWidth()));
|
||||
|
||||
for (int y = 0; y < stubHeight; y++) {
|
||||
roles.put(new Vector3i(0, y, 0), FungusCellRole.STEM);
|
||||
}
|
||||
|
||||
int fanY = stubHeight;
|
||||
boolean hasGill = FungusCapBuilder.blockOrPaletteSet(fungus.gillBlock(), fungus.gillPalette());
|
||||
boolean hasSpot = FungusCapBuilder.blockOrPaletteSet(fungus.spotBlock(), fungus.spotPalette());
|
||||
|
||||
for (int dx = -radius; dx <= radius; dx++) {
|
||||
for (int dz = -radius; dz <= radius; dz++) {
|
||||
double dist = Math.sqrt(dx * dx + dz * dz);
|
||||
if (dist > radius + 0.5) {
|
||||
continue;
|
||||
}
|
||||
double forward = dx * fanX + dz * fanZ;
|
||||
if (forward < -0.5) {
|
||||
continue;
|
||||
}
|
||||
double normalized = dist / Math.max(1.0, radius);
|
||||
int lift = (int) Math.round(radius * 0.25 * (1.0 - normalized));
|
||||
int y = fanY + lift;
|
||||
|
||||
FungusCellRole role = FungusCellRole.CAP;
|
||||
if (hasSpot && spotNoise(dx, y, dz, seed + 9311L) <= fungus.spotChance()) {
|
||||
role = FungusCellRole.SPOT;
|
||||
}
|
||||
roles.putIfAbsent(new Vector3i(dx, y, dz), role);
|
||||
|
||||
if (hasGill && rollNoise(dx, y - 1, dz, seed + 4111L) <= fungus.gillChance()) {
|
||||
roles.putIfAbsent(new Vector3i(dx, y - 1, dz), FungusCellRole.GILL);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static double rollNoise(int x, int y, int z, long seed) {
|
||||
return TreeFunctions.valueNoise3D(x, y, z, seed);
|
||||
}
|
||||
|
||||
private static double spotNoise(int x, int y, int z, long seed) {
|
||||
return TreeFunctions.valueNoise3D(Math.floorDiv(x, 2), y, Math.floorDiv(z, 2), seed);
|
||||
}
|
||||
}
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.fungi;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.tree.TreeFunctions;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import com.volmit.iris.util.math.Vector3i;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public final class FungusStemBuilder {
|
||||
private FungusStemBuilder() {
|
||||
}
|
||||
|
||||
public static double[] build(Map<Vector3i, BlockData> stemCells, Fungus fungus, int stemHeight, long seed) {
|
||||
int width = Math.max(1, Math.min(3, fungus.stemWidth()));
|
||||
double maxLean = stemHeight * Math.tan(Math.toRadians(Math.max(0.0, fungus.stemCurve())));
|
||||
double leanRad = Math.toRadians(fungus.stemLeanAzimuth());
|
||||
double leanX = Math.sin(leanRad);
|
||||
double leanZ = Math.cos(leanRad);
|
||||
double waveAmp = Math.max(0.0, fungus.stemWaveAmplitude());
|
||||
double wavePeriods = fungus.stemWavePeriods() == 0 ? 1.0 : fungus.stemWavePeriods();
|
||||
double waveAzimuth = Math.toRadians(fungus.stemLeanAzimuth() + 90.0);
|
||||
double waveX = Math.sin(waveAzimuth);
|
||||
double waveZ = Math.cos(waveAzimuth);
|
||||
|
||||
double topCx = 0.0;
|
||||
double topCz = 0.0;
|
||||
int topY = Math.max(0, stemHeight - 1);
|
||||
|
||||
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 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);
|
||||
}
|
||||
|
||||
topCx = cx;
|
||||
topCz = cz;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.fungi;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBuilder;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.engine.object.IrisObject;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralShape;
|
||||
import com.volmit.iris.engine.object.procedural.ResolvedProceduralPlacement;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** A fungus made placeable: its variants are baked by {@link FungusGenerator}. */
|
||||
public final class ResolvedFungus extends ResolvedProceduralPlacement {
|
||||
private final Fungus settings;
|
||||
|
||||
public ResolvedFungus(Fungus settings, IrisData data) {
|
||||
super(new Shape("fungus", settings.name(), settings.chance(), settings.density(), settings.seed(), settings.variants(), settings.mode(),
|
||||
settings.rotation(), settings.clamp(), settings.carvingSupport(), settings.underwater(), settings.translate(),
|
||||
settings.stiltSettings().orElse(null)), data);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ProceduralShape generate(int index, RNG rng, ObjectBuilder blocks) {
|
||||
throw new UnsupportedOperationException("A fungus bakes its variants itself, from the stem heights");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<IrisObject> bake(ObjectBuilder blocks) {
|
||||
List<IrisObject> baked = new ArrayList<>();
|
||||
int count = variantCount();
|
||||
int lo = Math.min(settings.stemHeightMin(), settings.stemHeightMax());
|
||||
int hi = Math.max(settings.stemHeightMin(), settings.stemHeightMax());
|
||||
RNG heightRng = new RNG(seed());
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
int height;
|
||||
if (count == 1 || lo == hi) {
|
||||
height = heightRng.i(lo, hi + 1);
|
||||
} else {
|
||||
double step = (hi - lo) / (double) (count - 1);
|
||||
double base = lo + step * i;
|
||||
double jitter = heightRng.d(-step * 0.3, step * 0.3);
|
||||
height = (int) Math.round(Math.max(lo, Math.min(hi, base + jitter)));
|
||||
}
|
||||
|
||||
ProceduralShape generated = FungusGenerator.generate(settings, Math.max(1, height), variantRng(i), blocks);
|
||||
if (generated == null || generated.blocks().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
baked.add(build(blocks, variantKey(i), generated));
|
||||
}
|
||||
|
||||
return baked;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.ruin;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBuilder;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralShape;
|
||||
import com.volmit.iris.engine.object.procedural.ResolvedProceduralPlacement;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
/** A ruin made placeable: its variants are baked by {@link RuinGenerator}. */
|
||||
public final class ResolvedRuin extends ResolvedProceduralPlacement {
|
||||
private final Ruin settings;
|
||||
|
||||
public ResolvedRuin(Ruin settings, IrisData data) {
|
||||
super(new Shape("ruin", settings.name(), settings.chance(), settings.density(), settings.seed(), settings.variants(), settings.mode(),
|
||||
settings.rotation(), settings.clamp(), settings.carvingSupport(), settings.underwater(), settings.translate(),
|
||||
settings.stiltSettings().orElse(null)), data);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ProceduralShape generate(int index, RNG rng, ObjectBuilder blocks) {
|
||||
return RuinGenerator.generate(settings, index, rng, blocks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.ruin;
|
||||
|
||||
import com.volmit.iris.engine.object.ObjectPlaceMode;
|
||||
import com.volmit.iris.engine.object.CarvingMode;
|
||||
import com.volmit.iris.engine.object.IrisObjectRotation;
|
||||
import com.volmit.iris.engine.object.IrisObjectTranslate;
|
||||
import com.volmit.iris.engine.object.IrisStiltSettings;
|
||||
import com.volmit.iris.engine.object.IrisObjectLimit;
|
||||
import com.volmit.iris.engine.object.IrisMaterialPalette;
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
import com.volmit.iris.engine.object.annotations.Snippet;
|
||||
import com.volmit.iris.engine.object.annotations.MaxNumber;
|
||||
import com.volmit.iris.engine.object.annotations.ArrayType;
|
||||
import com.volmit.iris.engine.object.annotations.MinNumber;
|
||||
import com.volmit.iris.engine.object.annotations.RegistryListBlockType;
|
||||
import com.volmit.iris.engine.object.annotations.Required;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Snippet("ruin")
|
||||
@Desc("A single procedurally generated ruin: a crumbling man-made structure (pillar, wall, arch, foundation slab, or rubble pile). Iris bakes a pool of deterministic variants from these settings and scatters them at world-gen time, exactly like an object placement but generated from scratch instead of loaded from an iob file. Weathering swaps blocks for mossy or cracked variants by noise, erosion deletes blocks so the shape reads as collapsed, and a buried fraction sinks the ruin into the terrain.")
|
||||
public final class Ruin {
|
||||
@Desc("A human readable name used in logs and as the variant load key (procedural/<name>#<index>).")
|
||||
private String name = "ruin";
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The chance (0-1) per chunk for this ruin to attempt placement. Use density to attempt more than one per chunk once the chance check passes.")
|
||||
private double chance = 0.05;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("If the chance check passes, attempt this many placements in the chunk. Defaults to a single ruin per qualifying chunk.")
|
||||
private int density = 1;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("How many distinct variants to pre-bake for this ruin. Higher means more variety (different heights, gap patterns, erosion seeds) at a small memory cost.")
|
||||
private int variants = 6;
|
||||
|
||||
@Desc("The base seed for deterministic generation. The same seed and settings always bake the same variants. Every random choice (size, weathering mask, erosion mask, accents) is derived from this.")
|
||||
private long seed = 1337;
|
||||
|
||||
@Desc("The placement mode used to anchor the ruin to the terrain. MIN_HEIGHT plants the lowest footprint corner on the ground (good for slabs and rubble on slopes); CENTER_HEIGHT averages the footprint (good for tall pillars and arches).")
|
||||
private ObjectPlaceMode mode = ObjectPlaceMode.MIN_HEIGHT;
|
||||
|
||||
@Desc("Rotate this ruin's placement so variants do not all face the same way.")
|
||||
private IrisObjectRotation rotation = new IrisObjectRotation();
|
||||
|
||||
@Desc("Limit the max or min terrain height at which this ruin may place.")
|
||||
private IrisObjectLimit clamp = new IrisObjectLimit();
|
||||
|
||||
@Desc("Whether this ruin may place on the terrain surface, under carvings (caves), or both. SURFACE_ONLY keeps ruins above ground.")
|
||||
private CarvingMode carvingSupport = CarvingMode.SURFACE_ONLY;
|
||||
|
||||
@Desc("If true the ruin anchors on the terrain height ignoring the water surface, so it can sit submerged. If false (default) it anchors to the surface and avoids water.")
|
||||
private boolean underwater = false;
|
||||
|
||||
@Desc("Translate (offset) this placement along each axis; for example set a negative y to sink it into the ground.")
|
||||
private IrisObjectTranslate translate = new IrisObjectTranslate();
|
||||
|
||||
@Desc("Settings for the stilt place modes (STILT, MIN_STILT, FAST_STILT, CENTER_STILT, ERODE_STILT, ORGANIC_STILT).")
|
||||
private IrisStiltSettings stiltSettings;
|
||||
|
||||
@Required
|
||||
@Desc("The primary structural block, e.g. minecraft:cobblestone or minecraft:stone_bricks. Ignored when blockPalette is set. This is the bulk material of the ruin before weathering swaps some of it out.")
|
||||
@RegistryListBlockType
|
||||
private String block = "minecraft:cobblestone";
|
||||
|
||||
@Desc("A noise-driven palette for the primary block. When set this overrides the single block, letting the structure mix materials by noise. Palette wins over the block string via IrisProceduralBlocks.resolve.")
|
||||
private IrisMaterialPalette blockPalette;
|
||||
|
||||
@Desc("The overall silhouette of the ruin: PILLAR (broken column), WALL (gapped segment), ARCH (two legs plus a curved span), FLOOR_SLAB (foundation patch), or RUBBLE (low scattered pile).")
|
||||
private RuinForm form = RuinForm.PILLAR;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Minimum structure height in blocks (PILLAR/WALL/ARCH height, slab thickness for FLOOR_SLAB, mound height for RUBBLE). Variants interpolate between this and heightMax.")
|
||||
private int heightMin = 4;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Maximum structure height in blocks. Variants interpolate between heightMin and this so taller and shorter ruins coexist.")
|
||||
private int heightMax = 9;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Minimum footprint width (X axis) in blocks: column/leg thickness for PILLAR/ARCH, wall thickness for WALL, slab width for FLOOR_SLAB, blob width for RUBBLE.")
|
||||
private int widthMin = 1;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Maximum footprint width (X axis) in blocks. Variants interpolate between widthMin and this.")
|
||||
private int widthMax = 3;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Minimum footprint length (Z axis) in blocks: wall run length, arch leg spacing, slab length, rubble blob length.")
|
||||
private int lengthMin = 3;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Maximum footprint length (Z axis) in blocks. Variants interpolate between lengthMin and this.")
|
||||
private int lengthMax = 7;
|
||||
|
||||
@Desc("A palette of weathered variants (mossy/cracked block ids) blended over the structure by noise. When set this drives weathering; otherwise weatheredBlock is used.")
|
||||
private IrisMaterialPalette weatheringPalette;
|
||||
|
||||
@Desc("A single weathered block id used when weatheringPalette is not set, e.g. minecraft:mossy_cobblestone. Applied to a noise-selected fraction of the structure, biased toward the lower rows.")
|
||||
@RegistryListBlockType
|
||||
private String weatheredBlock = "minecraft:mossy_cobblestone";
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("How much of the structure is replaced by weathered blocks (0 none, 1 nearly all). The replacement mask is value-noise driven and weighted so lower rows weather more than upper rows, mimicking moss climbing from the ground.")
|
||||
private double mossiness = 0.45;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(8)
|
||||
@Desc("The scale of the weathering noise. Higher values produce smaller, busier moss/crack patches; lower values produce broad continuous weathered zones.")
|
||||
private double weatheringScale = 1.0;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("How crumbled the ruin is (0 intact, 1 mostly gone). Blocks whose value-noise falls below this threshold are deleted, so the shape reads as collapsed. Structural integrity is preserved: the bottom row and core legs are never eroded away.")
|
||||
private double erosion = 0.25;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(8)
|
||||
@Desc("The scale of the erosion noise. Higher values knock out small speckled holes; lower values carve larger missing chunks out of the silhouette.")
|
||||
private double erosionScale = 1.5;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("How far the structure extends below the surface as a fraction of its height (0 sits fully on top, 1 sinks it a full height into the ground). The buried rows live at negative y so the ruin reads as settled and partly swallowed by the terrain.")
|
||||
private double buriedFraction = 0.2;
|
||||
|
||||
@ArrayType(min = 1, type = RuinDecorator.class)
|
||||
@Desc("Accent decorators applied after the ruin is built and eroded (moss carpets and lanterns on broken tops, vines and lichen on the faces, rubble and growth scattered around the base).")
|
||||
private List<RuinDecorator> accents = List.of();
|
||||
|
||||
public Ruin() {
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public double chance() {
|
||||
return chance;
|
||||
}
|
||||
|
||||
public int density() {
|
||||
return density;
|
||||
}
|
||||
|
||||
public int variants() {
|
||||
return variants;
|
||||
}
|
||||
|
||||
public long seed() {
|
||||
return seed;
|
||||
}
|
||||
|
||||
public ObjectPlaceMode mode() {
|
||||
return mode == null ? ObjectPlaceMode.MIN_HEIGHT : mode;
|
||||
}
|
||||
|
||||
public IrisObjectRotation rotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public IrisObjectLimit clamp() {
|
||||
return clamp;
|
||||
}
|
||||
|
||||
public CarvingMode carvingSupport() {
|
||||
return carvingSupport == null ? CarvingMode.SURFACE_ONLY : carvingSupport;
|
||||
}
|
||||
|
||||
public boolean underwater() {
|
||||
return underwater;
|
||||
}
|
||||
|
||||
public IrisObjectTranslate translate() {
|
||||
return translate;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisStiltSettings> stiltSettings() {
|
||||
return Optional.ofNullable(stiltSettings);
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
|
||||
public String block() {
|
||||
return block;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisMaterialPalette> blockPalette() {
|
||||
return Optional.ofNullable(blockPalette);
|
||||
}
|
||||
|
||||
public RuinForm form() {
|
||||
return form == null ? RuinForm.PILLAR : form;
|
||||
}
|
||||
|
||||
public int heightMin() {
|
||||
return heightMin;
|
||||
}
|
||||
|
||||
public int heightMax() {
|
||||
return heightMax;
|
||||
}
|
||||
|
||||
public int widthMin() {
|
||||
return widthMin;
|
||||
}
|
||||
|
||||
public int widthMax() {
|
||||
return widthMax;
|
||||
}
|
||||
|
||||
public int lengthMin() {
|
||||
return lengthMin;
|
||||
}
|
||||
|
||||
public int lengthMax() {
|
||||
return lengthMax;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisMaterialPalette> weatheringPalette() {
|
||||
return Optional.ofNullable(weatheringPalette);
|
||||
}
|
||||
|
||||
public String weatheredBlock() {
|
||||
return weatheredBlock;
|
||||
}
|
||||
|
||||
public double mossiness() {
|
||||
return mossiness;
|
||||
}
|
||||
|
||||
public double weatheringScale() {
|
||||
return weatheringScale;
|
||||
}
|
||||
|
||||
public double erosion() {
|
||||
return erosion;
|
||||
}
|
||||
|
||||
public double erosionScale() {
|
||||
return erosionScale;
|
||||
}
|
||||
|
||||
public double buriedFraction() {
|
||||
return buriedFraction;
|
||||
}
|
||||
|
||||
public List<RuinDecorator> accents() {
|
||||
return accents == null ? List.of() : Collections.unmodifiableList(accents);
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.ruin;
|
||||
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import com.volmit.iris.util.math.Vector3i;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
final class RuinBlockCanvas {
|
||||
enum Role {
|
||||
STRUCTURE,
|
||||
ACCENT
|
||||
}
|
||||
|
||||
static final class Cell {
|
||||
private Role role;
|
||||
private boolean structural;
|
||||
private BlockData accentData;
|
||||
|
||||
Cell(Role role, boolean structural) {
|
||||
this.role = role;
|
||||
this.structural = structural;
|
||||
}
|
||||
|
||||
Role role() {
|
||||
return role;
|
||||
}
|
||||
|
||||
boolean structural() {
|
||||
return structural;
|
||||
}
|
||||
|
||||
BlockData accentData() {
|
||||
return accentData;
|
||||
}
|
||||
|
||||
void promote(Role next, boolean structuralNext) {
|
||||
this.role = next;
|
||||
this.structural = this.structural || structuralNext;
|
||||
}
|
||||
|
||||
void makeAccent(BlockData data) {
|
||||
this.role = Role.ACCENT;
|
||||
this.structural = false;
|
||||
this.accentData = data;
|
||||
}
|
||||
}
|
||||
|
||||
private final Map<Vector3i, Cell> cells = new HashMap<>();
|
||||
|
||||
Map<Vector3i, Cell> cells() {
|
||||
return cells;
|
||||
}
|
||||
|
||||
void set(int x, int y, int z, Role role, boolean structural) {
|
||||
Vector3i key = new Vector3i(x, y, z);
|
||||
Cell existing = cells.get(key);
|
||||
if (existing == null) {
|
||||
cells.put(key, new Cell(role, structural));
|
||||
} else {
|
||||
existing.promote(role, structural);
|
||||
}
|
||||
}
|
||||
|
||||
void accent(int x, int y, int z, BlockData data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
cells.put(new Vector3i(x, y, z), accentCell(data));
|
||||
}
|
||||
|
||||
private static Cell accentCell(BlockData data) {
|
||||
Cell cell = new Cell(Role.ACCENT, false);
|
||||
cell.makeAccent(data);
|
||||
return cell;
|
||||
}
|
||||
|
||||
boolean has(int x, int y, int z) {
|
||||
return cells.containsKey(new Vector3i(x, y, z));
|
||||
}
|
||||
|
||||
Cell get(int x, int y, int z) {
|
||||
return cells.get(new Vector3i(x, y, z));
|
||||
}
|
||||
|
||||
void remove(int x, int y, int z) {
|
||||
cells.remove(new Vector3i(x, y, z));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.ruin;
|
||||
|
||||
import com.volmit.iris.engine.object.IrisMaterialPalette;
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
import com.volmit.iris.engine.object.annotations.Snippet;
|
||||
import com.volmit.iris.engine.object.annotations.MaxNumber;
|
||||
import com.volmit.iris.engine.object.annotations.MinNumber;
|
||||
import com.volmit.iris.engine.object.annotations.RegistryListBlockType;
|
||||
import com.volmit.iris.engine.object.annotations.Required;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Snippet("ruin-decorator")
|
||||
@Desc("An accent block applied to a generated ruin after it is built and eroded, such as moss carpets on broken tops, vines on the faces, or rubble scattered around the base.")
|
||||
public final class RuinDecorator {
|
||||
@Desc("Where on the ruin this accent is placed. TOP sits on the highest block of each column, SURFACE clings to air-facing vertical faces, BASE_SCATTER rings the ground footprint around the base.")
|
||||
private RuinDecoratorTarget target = RuinDecoratorTarget.TOP;
|
||||
|
||||
@Required
|
||||
@Desc("The block id to place, e.g. minecraft:moss_carpet or minecraft:vine. Ignored when palette is set.")
|
||||
@RegistryListBlockType
|
||||
private String block = "";
|
||||
|
||||
@Desc("A noise-driven palette for this decorator. When set this overrides the single block, letting the accent mix blocks by noise. Resolved through IrisProceduralBlocks.resolve so the palette wins over the string.")
|
||||
private IrisMaterialPalette palette;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The chance (0-1) per candidate position for this decorator to actually place. 1 covers every candidate, lower values leave sparse, patchy accents.")
|
||||
private double chance = 0.4;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("For the BASE_SCATTER target, how many blocks beyond the ruin footprint the scatter ring extends outward in each direction. Higher values fling rubble and growth further from the structure.")
|
||||
private int scatterRadius = 2;
|
||||
|
||||
public RuinDecorator() {
|
||||
}
|
||||
|
||||
public RuinDecoratorTarget target() {
|
||||
return target == null ? RuinDecoratorTarget.TOP : target;
|
||||
}
|
||||
|
||||
public String block() {
|
||||
return block;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisMaterialPalette> palette() {
|
||||
return Optional.ofNullable(palette);
|
||||
}
|
||||
|
||||
public double chance() {
|
||||
return chance;
|
||||
}
|
||||
|
||||
public int scatterRadius() {
|
||||
return scatterRadius;
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.ruin;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBuilder;
|
||||
import com.volmit.iris.engine.object.procedural.IrisProceduralBlocks;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import com.volmit.iris.util.math.Vector3i;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
final class RuinDecoratorApplier {
|
||||
private static final int[][] HORIZONTAL = {{1, 0, 0}, {-1, 0, 0}, {0, 0, 1}, {0, 0, -1}};
|
||||
|
||||
private RuinDecoratorApplier() {
|
||||
}
|
||||
|
||||
static void apply(RuinBlockCanvas canvas, RuinDecorator decorator, ObjectBuilder blocks, long seed, RNG rng) {
|
||||
List<Vector3i> candidates = switch (decorator.target()) {
|
||||
case TOP -> topCandidates(canvas);
|
||||
case SURFACE -> surfaceCandidates(canvas);
|
||||
case BASE_SCATTER -> baseScatterCandidates(canvas, Math.max(1, decorator.scatterRadius()));
|
||||
};
|
||||
|
||||
RNG paletteRng = new RNG(seed);
|
||||
for (Vector3i v : candidates) {
|
||||
if (!rng.chance(decorator.chance())) {
|
||||
continue;
|
||||
}
|
||||
BlockData bd = IrisProceduralBlocks.resolve(decorator.block(), decorator.palette(), blocks, v.getBlockX(), v.getBlockY(), v.getBlockZ(), paletteRng);
|
||||
if (bd != null) {
|
||||
canvas.accent(v.getBlockX(), v.getBlockY(), v.getBlockZ(), bd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Vector3i> topCandidates(RuinBlockCanvas canvas) {
|
||||
Map<Long, Integer> highest = new HashMap<>();
|
||||
for (Vector3i v : canvas.cells().keySet()) {
|
||||
long key = columnKey(v.getBlockX(), v.getBlockZ());
|
||||
Integer current = highest.get(key);
|
||||
if (current == null || v.getBlockY() > current) {
|
||||
highest.put(key, v.getBlockY());
|
||||
}
|
||||
}
|
||||
List<Vector3i> out = new ArrayList<>();
|
||||
for (Map.Entry<Long, Integer> entry : highest.entrySet()) {
|
||||
int x = (int) (entry.getKey() >> 32);
|
||||
int z = (int) (entry.getKey() & 0xFFFFFFFFL);
|
||||
int y = entry.getValue() + 1;
|
||||
if (!canvas.has(x, y, z)) {
|
||||
out.add(new Vector3i(x, y, z));
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static List<Vector3i> surfaceCandidates(RuinBlockCanvas canvas) {
|
||||
List<Vector3i> out = new ArrayList<>();
|
||||
for (Vector3i v : new ArrayList<>(canvas.cells().keySet())) {
|
||||
for (int[] dir : HORIZONTAL) {
|
||||
int nx = v.getBlockX() + dir[0];
|
||||
int ny = v.getBlockY();
|
||||
int nz = v.getBlockZ() + dir[2];
|
||||
if (!canvas.has(nx, ny, nz)) {
|
||||
out.add(new Vector3i(nx, ny, nz));
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static List<Vector3i> baseScatterCandidates(RuinBlockCanvas canvas, int radius) {
|
||||
int minX = Integer.MAX_VALUE;
|
||||
int minZ = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE;
|
||||
int maxZ = Integer.MIN_VALUE;
|
||||
int minY = Integer.MAX_VALUE;
|
||||
for (Vector3i v : canvas.cells().keySet()) {
|
||||
minX = Math.min(minX, v.getBlockX());
|
||||
minZ = Math.min(minZ, v.getBlockZ());
|
||||
maxX = Math.max(maxX, v.getBlockX());
|
||||
maxZ = Math.max(maxZ, v.getBlockZ());
|
||||
minY = Math.min(minY, v.getBlockY());
|
||||
}
|
||||
|
||||
List<Vector3i> out = new ArrayList<>();
|
||||
for (int x = minX - radius; x <= maxX + radius; x++) {
|
||||
for (int z = minZ - radius; z <= maxZ + radius; z++) {
|
||||
if (!canvas.has(x, minY, z) && !canvas.has(x, minY + 1, z)) {
|
||||
out.add(new Vector3i(x, minY, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static long columnKey(int x, int z) {
|
||||
return ((long) x << 32) | (z & 0xFFFFFFFFL);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.ruin;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Where a ruin accent block is placed after the ruin has been built and eroded.")
|
||||
public enum RuinDecoratorTarget {
|
||||
@Desc("On top of the highest surviving block in each column (moss carpets, snow, sculk, lanterns on broken tops).")
|
||||
TOP,
|
||||
@Desc("On air-facing vertical sides of any surviving block (vines, glow lichen, climbing growth on the ruin faces).")
|
||||
SURFACE,
|
||||
@Desc("Scattered across the ground footprint around the y=0 base ring, wider than the structure itself (loose rubble, mushrooms, grass tufts ringing the ruin).")
|
||||
BASE_SCATTER
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.ruin;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("The overall silhouette of a procedural ruin. Each form rasterizes a different crumbling man-made shape from the same size, weathering, and erosion settings.")
|
||||
public enum RuinForm {
|
||||
@Desc("A broken vertical column. A square trunk (widthMin..widthMax footprint) rises to a randomized height then snaps off with a jagged crown, leaving a toppled stub. Use for ancient pillars and obelisk stumps.")
|
||||
PILLAR,
|
||||
@Desc("A standing wall segment. A flat slab spanning lengthMin..lengthMax long and heightMin..heightMax tall, thickness widthMin..widthMax, punched through with noise-driven gaps and a ragged top edge. Use for collapsed building sides and fortifications.")
|
||||
WALL,
|
||||
@Desc("A freestanding arch. Two legs spaced by lengthMin..lengthMax joined at the top by a rasterized semicircular span (the lintel), so the structure literally arches. Use for gateways, aqueduct bays, and ruined doorways.")
|
||||
ARCH,
|
||||
@Desc("A flat foundation patch. A single (or few) block-thick slab covering a widthMin..lengthMax footprint at ground level, heavily eroded into an irregular outline. Use for floor remnants, plazas, and building foundations.")
|
||||
FLOOR_SLAB,
|
||||
@Desc("A low scattered debris pile. A noise blob of partial blocks mounded near the ground, tallest at the center and thinning toward the edges. Use for collapsed-structure rubble and scree.")
|
||||
RUBBLE
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.ruin;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBuilder;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralShape;
|
||||
import com.volmit.iris.engine.object.procedural.IrisProceduralBlocks;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import com.volmit.iris.engine.object.procedural.tree.TreeFunctions;
|
||||
import com.volmit.iris.util.math.Vector3i;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class RuinGenerator {
|
||||
private RuinGenerator() {
|
||||
}
|
||||
|
||||
public static ProceduralShape generate(Ruin ruin, int variantIndex, RNG rng, ObjectBuilder blocks) {
|
||||
int count = Math.max(1, ruin.variants());
|
||||
int height = pick(ruin.heightMin(), ruin.heightMax(), variantIndex, count, rng);
|
||||
int width = pick(ruin.widthMin(), ruin.widthMax(), variantIndex, count, rng);
|
||||
int length = pick(ruin.lengthMin(), ruin.lengthMax(), variantIndex, count, rng);
|
||||
|
||||
RuinBlockCanvas canvas = new RuinBlockCanvas();
|
||||
switch (ruin.form()) {
|
||||
case PILLAR -> RuinShapes.pillar(canvas, Math.max(1, width), Math.max(2, height), rng);
|
||||
case WALL -> RuinShapes.wall(canvas, Math.max(1, width), Math.max(2, length), Math.max(2, height), rng);
|
||||
case ARCH -> RuinShapes.arch(canvas, Math.max(1, width), Math.max(3, length), Math.max(3, height), rng);
|
||||
case FLOOR_SLAB -> RuinShapes.floorSlab(canvas, Math.max(3, width), Math.max(3, length), Math.max(1, Math.min(height, 2)));
|
||||
case RUBBLE -> RuinShapes.rubble(canvas, Math.max(3, width), Math.max(3, length), Math.max(1, Math.min(height, 4)), rng);
|
||||
}
|
||||
|
||||
applyBurial(ruin, canvas, height);
|
||||
applyErosion(ruin, canvas);
|
||||
applyAccents(ruin, canvas, rng, blocks);
|
||||
|
||||
return resolve(ruin, canvas, blocks);
|
||||
}
|
||||
|
||||
private static int pick(int lo, int hi, int variantIndex, int count, RNG rng) {
|
||||
int min = Math.min(lo, hi);
|
||||
int max = Math.max(lo, hi);
|
||||
if (min == max || count <= 1) {
|
||||
return rng.i(min, max + 1);
|
||||
}
|
||||
double step = (max - min) / (double) (count - 1);
|
||||
double base = min + step * variantIndex;
|
||||
double jitter = rng.d(-step * 0.3, step * 0.3);
|
||||
return (int) Math.round(Math.max(min, Math.min(max, base + jitter)));
|
||||
}
|
||||
|
||||
private static void applyBurial(Ruin ruin, RuinBlockCanvas canvas, int height) {
|
||||
double fraction = clamp01(ruin.buriedFraction());
|
||||
if (fraction <= 0.0) {
|
||||
return;
|
||||
}
|
||||
int sink = (int) Math.round(height * fraction);
|
||||
if (sink <= 0) {
|
||||
return;
|
||||
}
|
||||
Map<Vector3i, RuinBlockCanvas.Cell> moved = new HashMap<>();
|
||||
for (Map.Entry<Vector3i, RuinBlockCanvas.Cell> entry : canvas.cells().entrySet()) {
|
||||
Vector3i v = entry.getKey();
|
||||
moved.put(new Vector3i(v.getBlockX(), v.getBlockY() - sink, v.getBlockZ()), entry.getValue());
|
||||
}
|
||||
canvas.cells().clear();
|
||||
canvas.cells().putAll(moved);
|
||||
}
|
||||
|
||||
private static void applyErosion(Ruin ruin, RuinBlockCanvas canvas) {
|
||||
double erosion = clamp01(ruin.erosion());
|
||||
if (erosion <= 0.0) {
|
||||
return;
|
||||
}
|
||||
double scale = Math.max(0.0001, ruin.erosionScale());
|
||||
long erosionSeed = ruin.seed() * 0x2545F4914F6CDD1DL + 17L;
|
||||
int minY = Integer.MAX_VALUE;
|
||||
for (Vector3i v : canvas.cells().keySet()) {
|
||||
minY = Math.min(minY, v.getBlockY());
|
||||
}
|
||||
|
||||
List<Vector3i> doomed = new ArrayList<>();
|
||||
for (Map.Entry<Vector3i, RuinBlockCanvas.Cell> entry : canvas.cells().entrySet()) {
|
||||
Vector3i v = entry.getKey();
|
||||
RuinBlockCanvas.Cell cell = entry.getValue();
|
||||
if (cell.structural() || v.getBlockY() <= minY) {
|
||||
continue;
|
||||
}
|
||||
int sx = (int) Math.round(v.getBlockX() * scale);
|
||||
int sy = (int) Math.round(v.getBlockY() * scale);
|
||||
int sz = (int) Math.round(v.getBlockZ() * scale);
|
||||
double n = TreeFunctions.valueNoise3D(sx, sy, sz, erosionSeed);
|
||||
if (n < erosion) {
|
||||
doomed.add(v);
|
||||
}
|
||||
}
|
||||
for (Vector3i v : doomed) {
|
||||
canvas.cells().remove(v);
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyAccents(Ruin ruin, RuinBlockCanvas canvas, RNG rng, ObjectBuilder blocks) {
|
||||
if (ruin.accents() == null || ruin.accents().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int decoratorIndex = 0;
|
||||
for (com.volmit.iris.engine.object.procedural.ruin.RuinDecorator decorator : ruin.accents()) {
|
||||
RuinDecoratorApplier.apply(canvas, decorator, blocks, ruin.seed() + decoratorIndex * 911L, rng.nextParallelRNG(decoratorIndex + 1));
|
||||
decoratorIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
private static ProceduralShape resolve(Ruin ruin, RuinBlockCanvas canvas, ObjectBuilder blocks) {
|
||||
if (canvas.cells().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
double scale = Math.max(0.0001, ruin.weatheringScale());
|
||||
double mossiness = clamp01(ruin.mossiness());
|
||||
long weatherSeed = ruin.seed() * 0x9E3779B97F4A7C15L + 101L;
|
||||
RNG paletteRng = new RNG(ruin.seed());
|
||||
|
||||
int minY = Integer.MAX_VALUE;
|
||||
int maxY = Integer.MIN_VALUE;
|
||||
for (Vector3i v : canvas.cells().keySet()) {
|
||||
minY = Math.min(minY, v.getBlockY());
|
||||
maxY = Math.max(maxY, v.getBlockY());
|
||||
}
|
||||
int span = Math.max(1, maxY - minY);
|
||||
|
||||
Map<Vector3i, BlockData> resolved = new HashMap<>();
|
||||
for (Map.Entry<Vector3i, RuinBlockCanvas.Cell> entry : canvas.cells().entrySet()) {
|
||||
Vector3i v = entry.getKey();
|
||||
RuinBlockCanvas.Cell cell = entry.getValue();
|
||||
BlockData bd;
|
||||
if (cell.role() == RuinBlockCanvas.Role.ACCENT) {
|
||||
bd = cell.accentData();
|
||||
} else {
|
||||
bd = structuralBlock(ruin, cell, v, blocks, paletteRng, scale, mossiness, weatherSeed, minY, span);
|
||||
}
|
||||
if (bd != null) {
|
||||
resolved.put(v, bd);
|
||||
}
|
||||
}
|
||||
|
||||
return IrisProceduralBlocks.assemble(resolved);
|
||||
}
|
||||
|
||||
private static BlockData structuralBlock(Ruin ruin, RuinBlockCanvas.Cell cell, Vector3i v, ObjectBuilder blocks, RNG paletteRng, double scale, double mossiness, long weatherSeed, int minY, int span) {
|
||||
boolean weathered = false;
|
||||
if (mossiness > 0.0) {
|
||||
int sx = (int) Math.round(v.getBlockX() * scale);
|
||||
int sy = (int) Math.round(v.getBlockY() * scale);
|
||||
int sz = (int) Math.round(v.getBlockZ() * scale);
|
||||
double n = TreeFunctions.valueNoise3D(sx, sy, sz, weatherSeed);
|
||||
double normalized = (v.getBlockY() - minY) / (double) span;
|
||||
double gradient = 1.0 - 0.6 * normalized;
|
||||
weathered = n < mossiness * gradient;
|
||||
}
|
||||
|
||||
if (weathered) {
|
||||
BlockData wd = IrisProceduralBlocks.resolve(ruin.weatheredBlock(), ruin.weatheringPalette(), blocks, v.getBlockX(), v.getBlockY(), v.getBlockZ(), paletteRng);
|
||||
if (wd != null) {
|
||||
return wd;
|
||||
}
|
||||
}
|
||||
return IrisProceduralBlocks.resolve(ruin.block(), ruin.blockPalette(), blocks, v.getBlockX(), v.getBlockY(), v.getBlockZ(), paletteRng);
|
||||
}
|
||||
|
||||
private static double clamp01(double value) {
|
||||
if (value < 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
if (value > 1.0) {
|
||||
return 1.0;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.ruin;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.tree.TreeFunctions;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
final class RuinShapes {
|
||||
private RuinShapes() {
|
||||
}
|
||||
|
||||
static void pillar(RuinBlockCanvas canvas, int width, int height, RNG rng) {
|
||||
int half = (width - 1) / 2;
|
||||
int broken = Math.max(2, height - rng.i(0, Math.max(1, height / 3)));
|
||||
for (int y = 0; y < broken; y++) {
|
||||
boolean bottom = y == 0;
|
||||
int jitter = y >= broken - 1 ? rng.i(0, 2) : 0;
|
||||
for (int x = -half; x <= half; x++) {
|
||||
for (int z = -half; z <= half; z++) {
|
||||
if (y == broken - 1 && jitter > 0 && (Math.abs(x) == half || Math.abs(z) == half) && rng.chance(0.5)) {
|
||||
continue;
|
||||
}
|
||||
canvas.set(x, y, z, RuinBlockCanvas.Role.STRUCTURE, bottom);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int stub = rng.i(0, 3);
|
||||
if (stub > 0) {
|
||||
int dir = rng.i(0, 4);
|
||||
int dx = dir == 0 ? 1 : (dir == 1 ? -1 : 0);
|
||||
int dz = dir == 2 ? 1 : (dir == 3 ? -1 : 0);
|
||||
for (int s = 1; s <= stub; s++) {
|
||||
canvas.set((half + s) * dx, 0, (half + s) * dz, RuinBlockCanvas.Role.STRUCTURE, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void wall(RuinBlockCanvas canvas, int thickness, int length, int height, RNG rng) {
|
||||
int half = (length - 1) / 2;
|
||||
int tHalf = (thickness - 1) / 2;
|
||||
long edgeSeed = rng.lmax();
|
||||
for (int z = -half; z <= half; z++) {
|
||||
int top = height - jaggedDrop(z, height, edgeSeed);
|
||||
for (int y = 0; y < top; y++) {
|
||||
if (y > 0 && rng.chance(0.10)) {
|
||||
continue;
|
||||
}
|
||||
for (int x = -tHalf; x <= thickness - 1 - tHalf; x++) {
|
||||
canvas.set(x, y, z, RuinBlockCanvas.Role.STRUCTURE, y == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void arch(RuinBlockCanvas canvas, int thickness, int span, int height, RNG rng) {
|
||||
int legSpacing = Math.max(2, span);
|
||||
int legGap = (legSpacing - 1) / 2;
|
||||
int tHalf = (thickness - 1) / 2;
|
||||
int legTop = Math.max(2, height - Math.max(2, (legSpacing / 2)));
|
||||
|
||||
for (int side = -1; side <= 1; side += 2) {
|
||||
int legZ = side * legGap;
|
||||
for (int y = 0; y < legTop; y++) {
|
||||
for (int x = -tHalf; x <= thickness - 1 - tHalf; x++) {
|
||||
canvas.set(x, y, legZ, RuinBlockCanvas.Role.STRUCTURE, y <= 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
double radius = legGap;
|
||||
int crown = legTop + (int) Math.round(radius);
|
||||
for (int z = -legGap; z <= legGap; z++) {
|
||||
double inside = radius * radius - (z * z);
|
||||
if (inside < 0) {
|
||||
continue;
|
||||
}
|
||||
int y = legTop + (int) Math.round(Math.sqrt(inside));
|
||||
for (int yy = legTop; yy <= y && yy <= crown; yy++) {
|
||||
boolean keystone = Math.abs(z) <= 1;
|
||||
for (int x = -tHalf; x <= thickness - 1 - tHalf; x++) {
|
||||
canvas.set(x, yy, z, RuinBlockCanvas.Role.STRUCTURE, keystone);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void floorSlab(RuinBlockCanvas canvas, int width, int length, int thickness) {
|
||||
int xHalf = (width - 1) / 2;
|
||||
int zHalf = (length - 1) / 2;
|
||||
for (int y = 0; y < thickness; y++) {
|
||||
for (int x = -xHalf; x <= width - 1 - xHalf; x++) {
|
||||
for (int z = -zHalf; z <= length - 1 - zHalf; z++) {
|
||||
canvas.set(x, y, z, RuinBlockCanvas.Role.STRUCTURE, y == 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void rubble(RuinBlockCanvas canvas, int width, int length, int peak, RNG rng) {
|
||||
int xHalf = (width - 1) / 2;
|
||||
int zHalf = (length - 1) / 2;
|
||||
long blobSeed = rng.lmax();
|
||||
double rx = Math.max(1.0, width / 2.0);
|
||||
double rz = Math.max(1.0, length / 2.0);
|
||||
|
||||
for (int x = -xHalf; x <= width - 1 - xHalf; x++) {
|
||||
for (int z = -zHalf; z <= length - 1 - zHalf; z++) {
|
||||
double radial = (x * x) / (rx * rx) + (z * z) / (rz * rz);
|
||||
if (radial > 1.2) {
|
||||
continue;
|
||||
}
|
||||
double falloff = Math.max(0.0, 1.0 - radial);
|
||||
double noise = com.volmit.iris.engine.object.procedural.tree.TreeFunctions.valueNoise3D(x, 0, z, blobSeed);
|
||||
int columnHeight = (int) Math.round(peak * falloff * (0.6 + 0.4 * noise));
|
||||
for (int y = 0; y <= columnHeight; y++) {
|
||||
if (y > 0 && rng.chance(0.25)) {
|
||||
continue;
|
||||
}
|
||||
canvas.set(x, y, z, RuinBlockCanvas.Role.STRUCTURE, y == 0 && radial < 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int jaggedDrop(int z, int height, long seed) {
|
||||
double n = com.volmit.iris.engine.object.procedural.tree.TreeFunctions.valueNoise1D(z * 0.9, seed);
|
||||
return (int) Math.round(n * Math.max(1, height - 1) * 0.6);
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBuilder;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralShape;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBlock;
|
||||
import com.volmit.iris.engine.object.procedural.BlockPosition;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ProceduralTreeGenerator {
|
||||
private ProceduralTreeGenerator() {
|
||||
}
|
||||
|
||||
public static ProceduralShape generate(Tree tree, int height, RNG rng, ObjectBuilder blocks) {
|
||||
TreeBlockCanvas canvas = new TreeBlockCanvas();
|
||||
long baseSeed = rng.getSeed();
|
||||
|
||||
List<TreeTrunkBuilder.Limb> limbs = TreeTrunkBuilder.build(canvas, tree, height);
|
||||
|
||||
boolean wantEndpoints = tree.decorators() != null && !tree.decorators().isEmpty();
|
||||
List<int[]> branchEndpoints = wantEndpoints ? new ArrayList<>() : null;
|
||||
|
||||
int limbIndex = 0;
|
||||
for (TreeTrunkBuilder.Limb limb : limbs) {
|
||||
TreeCanopyBuilder.build(canvas, tree, height, limb.offsets(), limb.branchStartY(), baseSeed + limbIndex * 131L, branchEndpoints);
|
||||
limbIndex++;
|
||||
}
|
||||
|
||||
if (wantEndpoints) {
|
||||
TreeDecoratorApplier.apply(canvas, tree, baseSeed, branchEndpoints);
|
||||
}
|
||||
|
||||
if (tree.roots()) {
|
||||
TreeRootBuilder.build(canvas, tree, height, baseSeed);
|
||||
}
|
||||
|
||||
if (tree.plausible()) {
|
||||
TreeSupport.ensureLeavesSupported(canvas, 24);
|
||||
}
|
||||
|
||||
Map<TreeBlockCanvas.Vec, BlockData> resolved = new HashMap<>();
|
||||
Set<TreeBlockCanvas.Vec> trunkPositions = new HashSet<>();
|
||||
Set<TreeBlockCanvas.Vec> leafPositions = new HashSet<>();
|
||||
for (Map.Entry<TreeBlockCanvas.Vec, TreeBlockCanvas.Cell> entry : canvas.getCells().entrySet()) {
|
||||
TreeBlockCanvas.Cell cell = entry.getValue();
|
||||
BlockData state = TreeBlockResolver.resolve(tree, blocks, cell, entry.getKey());
|
||||
if (state == null) {
|
||||
continue;
|
||||
}
|
||||
resolved.put(entry.getKey(), state);
|
||||
if (cell.role() == TreeBlockCanvas.Role.TRUNK || cell.role() == TreeBlockCanvas.Role.SECONDARY_TRUNK) {
|
||||
trunkPositions.add(entry.getKey());
|
||||
} else if (cell.role() == TreeBlockCanvas.Role.LEAF || cell.role() == TreeBlockCanvas.Role.SECONDARY_LEAF) {
|
||||
leafPositions.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
TreePlausibility.apply(resolved, trunkPositions, leafPositions, tree, blocks);
|
||||
|
||||
return assemble(resolved);
|
||||
}
|
||||
|
||||
private static ProceduralShape assemble(Map<TreeBlockCanvas.Vec, BlockData> resolved) {
|
||||
if (resolved.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int minX = Integer.MAX_VALUE;
|
||||
int minY = Integer.MAX_VALUE;
|
||||
int minZ = Integer.MAX_VALUE;
|
||||
int maxX = Integer.MIN_VALUE;
|
||||
int maxY = Integer.MIN_VALUE;
|
||||
int maxZ = Integer.MIN_VALUE;
|
||||
for (TreeBlockCanvas.Vec v : resolved.keySet()) {
|
||||
minX = Math.min(minX, v.x());
|
||||
minY = Math.min(minY, v.y());
|
||||
minZ = Math.min(minZ, v.z());
|
||||
maxX = Math.max(maxX, v.x());
|
||||
maxY = Math.max(maxY, v.y());
|
||||
maxZ = Math.max(maxZ, v.z());
|
||||
}
|
||||
|
||||
int w = maxX - minX + 1;
|
||||
int h = maxY - minY + 1;
|
||||
int d = maxZ - minZ + 1;
|
||||
int cx = w / 2;
|
||||
int cy = h / 2;
|
||||
int cz = d / 2;
|
||||
|
||||
List<ObjectBlock> placed = new ArrayList<>(resolved.size());
|
||||
for (Map.Entry<TreeBlockCanvas.Vec, BlockData> entry : resolved.entrySet()) {
|
||||
TreeBlockCanvas.Vec v = entry.getKey();
|
||||
int nx = v.x() - minX - cx;
|
||||
int ny = v.y() - cy + 1;
|
||||
int nz = v.z() - minZ - cz;
|
||||
placed.add(new ObjectBlock(new BlockPosition(nx, ny, nz), entry.getValue()));
|
||||
}
|
||||
|
||||
return new ProceduralShape(w, h, d, placed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBuilder;
|
||||
import com.volmit.iris.core.loader.IrisData;
|
||||
import com.volmit.iris.engine.object.IrisObject;
|
||||
import com.volmit.iris.engine.object.procedural.ProceduralShape;
|
||||
import com.volmit.iris.engine.object.procedural.ResolvedProceduralPlacement;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** A procedural tree made placeable: its variants are baked by {@link ProceduralTreeGenerator}, at heights spread between its limits. */
|
||||
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;
|
||||
|
||||
private final Tree settings;
|
||||
|
||||
public ResolvedTree(Tree settings, IrisData data) {
|
||||
super(new Shape("tree", settings.name(), settings.chance(), settings.density(), settings.seed(), settings.variants(), settings.mode(),
|
||||
settings.rotation(), settings.clamp(), settings.carvingSupport(), settings.underwater(), settings.translate(),
|
||||
settings.stiltSettings().orElse(null)), data);
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
protected ProceduralShape generate(int index, RNG rng, ObjectBuilder blocks) {
|
||||
throw new UnsupportedOperationException("Tree variants are baked with heights spread over the limits");
|
||||
}
|
||||
|
||||
@Override
|
||||
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());
|
||||
RNG heightRng = new RNG(settings.seed());
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
int height = variantHeight(heightRng, lo, hi, count, i);
|
||||
ProceduralShape generated = ProceduralTreeGenerator.generate(settings, Math.max(MINIMUM_HEIGHT, height),
|
||||
new RNG(settings.seed() + (i * VARIANT_SEED_STRIDE)), blocks);
|
||||
if (generated == null || generated.blocks().isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
baked.add(build(blocks, variantKey(i), generated));
|
||||
}
|
||||
|
||||
return baked;
|
||||
}
|
||||
|
||||
private static int variantHeight(RNG heightRng, int lo, int hi, int count, int index) {
|
||||
if (count == 1 || lo == hi) {
|
||||
return heightRng.i(lo, hi + 1);
|
||||
}
|
||||
double step = (hi - lo) / (double) (count - 1);
|
||||
double base = lo + step * index;
|
||||
double jitter = heightRng.d(-step * HEIGHT_JITTER_FRACTION, step * HEIGHT_JITTER_FRACTION);
|
||||
return (int) Math.round(Math.max(lo, Math.min(hi, base + jitter)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.ObjectPlaceMode;
|
||||
import com.volmit.iris.engine.object.CarvingMode;
|
||||
import com.volmit.iris.engine.object.IrisObjectRotation;
|
||||
import com.volmit.iris.engine.object.IrisObjectTranslate;
|
||||
import com.volmit.iris.engine.object.IrisStiltSettings;
|
||||
import com.volmit.iris.engine.object.IrisObjectLimit;
|
||||
import com.volmit.iris.engine.object.IrisMaterialPalette;
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
import com.volmit.iris.engine.object.annotations.MaxNumber;
|
||||
import com.volmit.iris.engine.object.annotations.ArrayType;
|
||||
import com.volmit.iris.engine.object.annotations.MinNumber;
|
||||
import com.volmit.iris.engine.object.annotations.RegistryListBlockType;
|
||||
import com.volmit.iris.engine.object.annotations.Required;
|
||||
import com.volmit.iris.engine.object.annotations.Snippet;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Snippet("procedural-tree")
|
||||
@Desc("A single procedurally generated tree. Iris bakes a pool of deterministic variants from these settings and scatters them at world-gen time, exactly like an object placement but generated from scratch instead of loaded from an iob file.")
|
||||
public final class Tree {
|
||||
@Desc("A human readable name used in logs and as the variant load key.")
|
||||
private String name = "procedural-tree";
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The chance per chunk for this tree to attempt placement. Use density for multiple per chunk.")
|
||||
private double chance = 0.4;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("If the chance check passes, attempt this many placements in the chunk.")
|
||||
private int density = 1;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(64)
|
||||
@Desc("How many distinct variants to pre-bake for this tree. Higher means more variety at a small memory cost.")
|
||||
private int variants = 8;
|
||||
|
||||
@Desc("The base seed for deterministic generation. The same seed and settings always bake the same variants.")
|
||||
private long seed = 1337;
|
||||
|
||||
@Desc("If true (default) leaves are placed with vanilla distance-from-wood values and persistent=false so leaf decay behaves naturally and the tree reads as plausible. If false, leaves are forced persistent and never decay (a raw object dump).")
|
||||
private boolean plausible = true;
|
||||
|
||||
@Desc("The placement mode used to anchor the tree to the terrain.")
|
||||
private ObjectPlaceMode mode = ObjectPlaceMode.CENTER_HEIGHT;
|
||||
|
||||
@Desc("Rotate this tree's placement.")
|
||||
private IrisObjectRotation rotation = new IrisObjectRotation();
|
||||
|
||||
@Desc("Limit the max or min height of placement.")
|
||||
private IrisObjectLimit clamp = new IrisObjectLimit();
|
||||
|
||||
@Desc("Whether this tree may place on the terrain surface, under carvings, or both.")
|
||||
private CarvingMode carvingSupport = CarvingMode.SURFACE_ONLY;
|
||||
|
||||
@Desc("If true, the tree anchors on the terrain height ignoring the water surface.")
|
||||
private boolean underwater = false;
|
||||
|
||||
@Desc("Translate (offset) this placement along each axis; for example set a negative y to sink it into the ground.")
|
||||
private IrisObjectTranslate translate = new IrisObjectTranslate();
|
||||
|
||||
@Desc("Settings for the stilt place modes (STILT, MIN_STILT, FAST_STILT, CENTER_STILT, ERODE_STILT, ORGANIC_STILT).")
|
||||
private IrisStiltSettings stiltSettings;
|
||||
|
||||
@Required
|
||||
@Desc("The trunk (log) block, e.g. minecraft:oak_log. Ignored when trunkPalette is set.")
|
||||
@RegistryListBlockType
|
||||
private String trunk = "minecraft:oak_log";
|
||||
|
||||
@Desc("A noise-driven palette for the trunk. When set this overrides the single trunk block, letting the trunk mix blocks by noise.")
|
||||
private IrisMaterialPalette trunkPalette;
|
||||
|
||||
@Required
|
||||
@Desc("The leaf block, e.g. minecraft:oak_leaves. Ignored when leavesPalette is set.")
|
||||
@RegistryListBlockType
|
||||
private String leaves = "minecraft:oak_leaves";
|
||||
|
||||
@Desc("A noise-driven palette for the leaves. When set this overrides the single leaf block, letting the canopy mix leaf (or other) blocks by noise. Only blocks that are actually leaves get vanilla decay distances.")
|
||||
private IrisMaterialPalette leavesPalette;
|
||||
|
||||
@Desc("The named canopy profile that drives default crown radii and shape.")
|
||||
private TreeProfile profile = TreeProfile.OAK;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Minimum trunk height in blocks.")
|
||||
private int heightMin = 8;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Maximum trunk height in blocks.")
|
||||
private int heightMax = 12;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Base trunk width in blocks. 1 is a single column, 2 is a 2x2 trunk, etc.")
|
||||
private int trunkWidth = 1;
|
||||
|
||||
@Desc("The function used to shape trunk width over height.")
|
||||
private TreeFunction trunkShape = TreeFunction.CONSTANT;
|
||||
|
||||
@Desc("Width multiplier at the base for the LINEAR trunk shape.")
|
||||
private double shapeStart = 1;
|
||||
|
||||
@Desc("Width multiplier at the top for the LINEAR trunk shape.")
|
||||
private double shapeEnd = 1;
|
||||
|
||||
@Desc("Steepness for the SIGMOID trunk shape.")
|
||||
private double shapeSteepness = 5;
|
||||
|
||||
@Desc("Base for the LOG trunk shape.")
|
||||
private double shapeBase = 2.718281828;
|
||||
|
||||
@Desc("Period for the SINE trunk shape.")
|
||||
private double shapePeriod = 1;
|
||||
|
||||
@Desc("Amplitude for the SINE trunk shape.")
|
||||
private double shapeAmplitude = 0.2;
|
||||
|
||||
@Desc("Pinch position (0-1) for the PARABOLIC trunk shape.")
|
||||
private double shapePeakOffset = 0.5;
|
||||
|
||||
@Desc("Minimum width fraction at the waist for the PARABOLIC trunk shape.")
|
||||
private double shapeFloor = 0.5;
|
||||
|
||||
@Desc("Compass direction in degrees the trunk leans toward.")
|
||||
private double leanAzimuth = 0;
|
||||
|
||||
@Desc("How far the trunk leans, in degrees from vertical. 0 is perfectly upright.")
|
||||
private double leanAngle = 0;
|
||||
|
||||
@Desc("How the lean accumulates over height.")
|
||||
private TreeFunction trunkCurve = TreeFunction.LINEAR;
|
||||
|
||||
@Desc("Steepness for the SIGMOID trunk curve.")
|
||||
private double curveSteepness = 8;
|
||||
|
||||
@Desc("How the lean azimuth itself changes over height (for spiraling or wandering trunks).")
|
||||
private TreeAzimuthMode leanAzimuthMode = TreeAzimuthMode.CONSTANT;
|
||||
|
||||
@Desc("Azimuth at the base for the LINEAR azimuth mode.")
|
||||
private double azimuthStart = 0;
|
||||
|
||||
@Desc("Azimuth at the top for the LINEAR azimuth mode.")
|
||||
private double azimuthEnd = 0;
|
||||
|
||||
@Desc("Number of full turns for the SPIRAL azimuth mode.")
|
||||
private double azimuthTurns = 1;
|
||||
|
||||
@Desc("Amplitude for the SINE azimuth mode.")
|
||||
private double azimuthAmplitude = 90;
|
||||
|
||||
@Desc("Period for the SINE azimuth mode.")
|
||||
private double azimuthPeriod = 1;
|
||||
|
||||
@Desc("Offset for the SINE azimuth mode.")
|
||||
private double azimuthOffset = 0;
|
||||
|
||||
@Desc("Scale for the NOISE azimuth mode.")
|
||||
private double azimuthScale = 1;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Number of branches per whorl ring for the WHORL branch azimuth mode.")
|
||||
private int azimuthWhorlCount = 5;
|
||||
|
||||
@Desc("Optional accent leaf block scattered through the canopy (blossoms, shroomlight).")
|
||||
@RegistryListBlockType
|
||||
private String secondaryLeaves;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The fraction of leaves replaced by the secondary leaf block(s).")
|
||||
private double secondaryLeafFraction = 0.35;
|
||||
|
||||
@ArrayType(min = 1, type = TreeSecondaryLeaf.class)
|
||||
@Desc("Optional weighted set of accent leaf blocks. When set this overrides the single secondaryLeaves block.")
|
||||
private List<TreeSecondaryLeaf> weightedSecondaryLeaves = List.of();
|
||||
|
||||
@Desc("A noise-driven palette for the secondary (accent) leaves. When set this overrides both secondaryLeaves and weightedSecondaryLeaves.")
|
||||
private IrisMaterialPalette secondaryLeavesPalette;
|
||||
|
||||
@Desc("Optional secondary trunk block used over a height band (for color-banded trunks). Ignored when secondaryTrunkPalette is set.")
|
||||
@RegistryListBlockType
|
||||
private String secondaryTrunk;
|
||||
|
||||
@Desc("A noise-driven palette for the secondary trunk band. When set this overrides the single secondaryTrunk block.")
|
||||
private IrisMaterialPalette secondaryTrunkPalette;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The normalized height where the secondary trunk band starts.")
|
||||
private double secondaryTrunkStart = 0.5;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The normalized height where the secondary trunk band ends.")
|
||||
private double secondaryTrunkEnd = 1;
|
||||
|
||||
@Desc("If true, a root system extends below (and around) the base so the tree connects to the ground on uneven terrain.")
|
||||
private boolean roots = true;
|
||||
|
||||
@Desc("How the root system is built when roots is true.")
|
||||
private TreeRootStyle rootStyle = TreeRootStyle.BUTTRESS;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Override the root depth/reach in blocks. 0 means auto (scales with tree height).")
|
||||
private int rootDepth = 0;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Override the root flare radius in blocks. 0 means auto.")
|
||||
private double rootFlare = 0;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(6)
|
||||
@Desc("How many separate trunks the tree splits into above forkHeight. 1 is a single trunk.")
|
||||
private int trunkForks = 1;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The normalized height where the trunk splits into forks.")
|
||||
private double forkHeight = 0.5;
|
||||
|
||||
@Desc("The outward lean angle in degrees of each fork.")
|
||||
private double forkAngle = 25;
|
||||
|
||||
@Desc("The leaf crown configuration.")
|
||||
private TreeCanopy canopy = new TreeCanopy();
|
||||
|
||||
@ArrayType(min = 1, type = TreeDecorator.class)
|
||||
@Desc("Accent decorators applied after the tree is built (fruit, vines, snow, glow).")
|
||||
private List<TreeDecorator> decorators = List.of();
|
||||
|
||||
public Tree() {
|
||||
}
|
||||
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public double chance() {
|
||||
return chance;
|
||||
}
|
||||
|
||||
public int density() {
|
||||
return density;
|
||||
}
|
||||
|
||||
public int variants() {
|
||||
return variants;
|
||||
}
|
||||
|
||||
public long seed() {
|
||||
return seed;
|
||||
}
|
||||
|
||||
public boolean plausible() {
|
||||
return plausible;
|
||||
}
|
||||
|
||||
public ObjectPlaceMode mode() {
|
||||
return mode;
|
||||
}
|
||||
|
||||
public IrisObjectRotation rotation() {
|
||||
return rotation;
|
||||
}
|
||||
|
||||
public IrisObjectLimit clamp() {
|
||||
return clamp;
|
||||
}
|
||||
|
||||
public CarvingMode carvingSupport() {
|
||||
return carvingSupport;
|
||||
}
|
||||
|
||||
public boolean underwater() {
|
||||
return underwater;
|
||||
}
|
||||
|
||||
public IrisObjectTranslate translate() {
|
||||
return translate;
|
||||
}
|
||||
|
||||
public Optional<IrisStiltSettings> stiltSettings() {
|
||||
return Optional.ofNullable(stiltSettings);
|
||||
}
|
||||
|
||||
public String trunk() {
|
||||
return trunk;
|
||||
}
|
||||
|
||||
public Optional<IrisMaterialPalette> trunkPalette() {
|
||||
return Optional.ofNullable(trunkPalette);
|
||||
}
|
||||
|
||||
public String leaves() {
|
||||
return leaves;
|
||||
}
|
||||
|
||||
public Optional<IrisMaterialPalette> leavesPalette() {
|
||||
return Optional.ofNullable(leavesPalette);
|
||||
}
|
||||
|
||||
public TreeProfile profile() {
|
||||
return profile;
|
||||
}
|
||||
|
||||
public int heightMin() {
|
||||
return heightMin;
|
||||
}
|
||||
|
||||
public int heightMax() {
|
||||
return heightMax;
|
||||
}
|
||||
|
||||
public int trunkWidth() {
|
||||
return trunkWidth;
|
||||
}
|
||||
|
||||
public TreeFunction trunkShape() {
|
||||
return trunkShape;
|
||||
}
|
||||
|
||||
public double shapeStart() {
|
||||
return shapeStart;
|
||||
}
|
||||
|
||||
public double shapeEnd() {
|
||||
return shapeEnd;
|
||||
}
|
||||
|
||||
public double shapeSteepness() {
|
||||
return shapeSteepness;
|
||||
}
|
||||
|
||||
public double shapeBase() {
|
||||
return shapeBase;
|
||||
}
|
||||
|
||||
public double shapePeriod() {
|
||||
return shapePeriod;
|
||||
}
|
||||
|
||||
public double shapeAmplitude() {
|
||||
return shapeAmplitude;
|
||||
}
|
||||
|
||||
public double shapePeakOffset() {
|
||||
return shapePeakOffset;
|
||||
}
|
||||
|
||||
public double shapeFloor() {
|
||||
return shapeFloor;
|
||||
}
|
||||
|
||||
public double leanAzimuth() {
|
||||
return leanAzimuth;
|
||||
}
|
||||
|
||||
public double leanAngle() {
|
||||
return leanAngle;
|
||||
}
|
||||
|
||||
public TreeFunction trunkCurve() {
|
||||
return trunkCurve;
|
||||
}
|
||||
|
||||
public double curveSteepness() {
|
||||
return curveSteepness;
|
||||
}
|
||||
|
||||
public TreeAzimuthMode leanAzimuthMode() {
|
||||
return leanAzimuthMode;
|
||||
}
|
||||
|
||||
public double azimuthStart() {
|
||||
return azimuthStart;
|
||||
}
|
||||
|
||||
public double azimuthEnd() {
|
||||
return azimuthEnd;
|
||||
}
|
||||
|
||||
public double azimuthTurns() {
|
||||
return azimuthTurns;
|
||||
}
|
||||
|
||||
public double azimuthAmplitude() {
|
||||
return azimuthAmplitude;
|
||||
}
|
||||
|
||||
public double azimuthPeriod() {
|
||||
return azimuthPeriod;
|
||||
}
|
||||
|
||||
public double azimuthOffset() {
|
||||
return azimuthOffset;
|
||||
}
|
||||
|
||||
public double azimuthScale() {
|
||||
return azimuthScale;
|
||||
}
|
||||
|
||||
public int azimuthWhorlCount() {
|
||||
return azimuthWhorlCount;
|
||||
}
|
||||
|
||||
public Optional<String> secondaryLeaves() {
|
||||
return Optional.ofNullable(secondaryLeaves);
|
||||
}
|
||||
|
||||
public double secondaryLeafFraction() {
|
||||
return secondaryLeafFraction;
|
||||
}
|
||||
|
||||
public List<TreeSecondaryLeaf> weightedSecondaryLeaves() {
|
||||
return weightedSecondaryLeaves == null ? List.of() : Collections.unmodifiableList(weightedSecondaryLeaves);
|
||||
}
|
||||
|
||||
public Optional<IrisMaterialPalette> secondaryLeavesPalette() {
|
||||
return Optional.ofNullable(secondaryLeavesPalette);
|
||||
}
|
||||
|
||||
public Optional<String> secondaryTrunk() {
|
||||
return Optional.ofNullable(secondaryTrunk);
|
||||
}
|
||||
|
||||
public Optional<IrisMaterialPalette> secondaryTrunkPalette() {
|
||||
return Optional.ofNullable(secondaryTrunkPalette);
|
||||
}
|
||||
|
||||
public double secondaryTrunkStart() {
|
||||
return secondaryTrunkStart;
|
||||
}
|
||||
|
||||
public double secondaryTrunkEnd() {
|
||||
return secondaryTrunkEnd;
|
||||
}
|
||||
|
||||
public boolean roots() {
|
||||
return roots;
|
||||
}
|
||||
|
||||
public TreeRootStyle rootStyle() {
|
||||
return rootStyle;
|
||||
}
|
||||
|
||||
public int rootDepth() {
|
||||
return rootDepth;
|
||||
}
|
||||
|
||||
public double rootFlare() {
|
||||
return rootFlare;
|
||||
}
|
||||
|
||||
public int trunkForks() {
|
||||
return trunkForks;
|
||||
}
|
||||
|
||||
public double forkHeight() {
|
||||
return forkHeight;
|
||||
}
|
||||
|
||||
public double forkAngle() {
|
||||
return forkAngle;
|
||||
}
|
||||
|
||||
public TreeCanopy canopy() {
|
||||
return canopy;
|
||||
}
|
||||
|
||||
public List<TreeDecorator> decorators() {
|
||||
return decorators == null ? List.of() : Collections.unmodifiableList(decorators);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Controls the compass direction (azimuth) of trunk lean as it rises, or the direction each branch is thrown.")
|
||||
public enum TreeAzimuthMode {
|
||||
@Desc("A single fixed azimuth.")
|
||||
CONSTANT,
|
||||
@Desc("Azimuth ramps straight from start to end with height.")
|
||||
LINEAR,
|
||||
@Desc("Azimuth winds around the trunk a number of turns (corkscrew).")
|
||||
SPIRAL,
|
||||
@Desc("Azimuth oscillates with a sine wave.")
|
||||
SINE,
|
||||
@Desc("Azimuth wanders using deterministic noise.")
|
||||
NOISE,
|
||||
@Desc("Each branch picks a uniformly random azimuth (falls back to constant for trunk lean).")
|
||||
RANDOM,
|
||||
@Desc("Phyllotaxis: each successive branch is offset by the 137.5 degree golden angle for the most natural spiral packing (branches).")
|
||||
GOLDEN_ANGLE,
|
||||
@Desc("Each successive branch is thrown to the opposite side (180 degrees apart).")
|
||||
ALTERNATING,
|
||||
@Desc("Branches are grouped into evenly spaced whorls of azimuthWhorlCount around each ring.")
|
||||
WHORL,
|
||||
@Desc("Azimuth zigzags by +/- amplitude on alternating branches.")
|
||||
ZIGZAG
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class TreeBlockCanvas {
|
||||
public record Vec(int x, int y, int z) {
|
||||
}
|
||||
|
||||
public enum Role {
|
||||
TRUNK, SECONDARY_TRUNK, LEAF, SECONDARY_LEAF, DECORATOR
|
||||
}
|
||||
|
||||
public enum Axis {
|
||||
NONE, X, Y, Z
|
||||
}
|
||||
|
||||
public record Cell(Role role, Axis axis, boolean exposed, int decoratorIndex, String facing) {
|
||||
}
|
||||
|
||||
private final Map<Vec, Cell> cells = new HashMap<>();
|
||||
private final Set<Vec> trunk = new HashSet<>();
|
||||
private final Set<Vec> leaf = new HashSet<>();
|
||||
|
||||
public boolean has(int x, int y, int z) {
|
||||
return cells.containsKey(new Vec(x, y, z));
|
||||
}
|
||||
|
||||
public Cell get(int x, int y, int z) {
|
||||
return cells.get(new Vec(x, y, z));
|
||||
}
|
||||
|
||||
public void setTrunk(int x, int y, int z, Role role, Axis axis) {
|
||||
Vec v = new Vec(x, y, z);
|
||||
cells.put(v, new Cell(role, axis, false, -1, null));
|
||||
trunk.add(v);
|
||||
leaf.remove(v);
|
||||
}
|
||||
|
||||
public boolean setLeaf(int x, int y, int z, Role role) {
|
||||
Vec v = new Vec(x, y, z);
|
||||
if (cells.containsKey(v)) {
|
||||
return false;
|
||||
}
|
||||
cells.put(v, new Cell(role, Axis.NONE, false, -1, null));
|
||||
leaf.add(v);
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean setDecor(int x, int y, int z, int decoratorIndex, String facing) {
|
||||
Vec v = new Vec(x, y, z);
|
||||
if (cells.containsKey(v)) {
|
||||
return false;
|
||||
}
|
||||
cells.put(v, new Cell(Role.DECORATOR, Axis.NONE, false, decoratorIndex, facing));
|
||||
return true;
|
||||
}
|
||||
|
||||
public void markExposed(int x, int y, int z) {
|
||||
Vec v = new Vec(x, y, z);
|
||||
Cell c = cells.get(v);
|
||||
if (c != null && !c.exposed()) {
|
||||
cells.put(v, new Cell(c.role(), c.axis(), true, c.decoratorIndex(), c.facing()));
|
||||
}
|
||||
}
|
||||
|
||||
public Map<Vec, Cell> getCells() {
|
||||
return cells;
|
||||
}
|
||||
|
||||
public Set<Vec> getTrunk() {
|
||||
return trunk;
|
||||
}
|
||||
|
||||
public Set<Vec> getLeaf() {
|
||||
return leaf;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return cells.isEmpty();
|
||||
}
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBuilder;
|
||||
import com.volmit.iris.engine.object.IrisMaterialPalette;
|
||||
import com.volmit.iris.engine.object.procedural.IrisProceduralBlocks;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
public final class TreeBlockResolver {
|
||||
private TreeBlockResolver() {
|
||||
}
|
||||
|
||||
public static BlockData resolve(Tree tree, ObjectBuilder blocks, TreeBlockCanvas.Cell cell, TreeBlockCanvas.Vec pos) {
|
||||
RNG paletteRng = new RNG(tree.seed());
|
||||
RNG posRng = new RNG(tree.seed() ^ positionHash(pos));
|
||||
|
||||
switch (cell.role()) {
|
||||
case TRUNK -> {
|
||||
BlockData state = resolveBlock(tree.trunk(), tree.trunkPalette().orElse(null), blocks, pos, paletteRng);
|
||||
return finishTrunk(state, cell, blocks);
|
||||
}
|
||||
case SECONDARY_TRUNK -> {
|
||||
BlockData state = resolveBlock(tree.secondaryTrunk().orElse(null), tree.secondaryTrunkPalette().orElse(null), blocks, pos, paletteRng);
|
||||
if (state == null) {
|
||||
state = resolveBlock(tree.trunk(), tree.trunkPalette().orElse(null), blocks, pos, paletteRng);
|
||||
}
|
||||
return finishTrunk(state, cell, blocks);
|
||||
}
|
||||
case LEAF -> {
|
||||
return resolveBlock(tree.leaves(), tree.leavesPalette().orElse(null), blocks, pos, paletteRng);
|
||||
}
|
||||
case SECONDARY_LEAF -> {
|
||||
BlockData state = resolveSecondaryLeaf(tree, blocks, pos, paletteRng, posRng);
|
||||
if (state == null) {
|
||||
state = resolveBlock(tree.leaves(), tree.leavesPalette().orElse(null), blocks, pos, paletteRng);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
case DECORATOR -> {
|
||||
int index = cell.decoratorIndex();
|
||||
if (index < 0 || tree.decorators() == null || index >= tree.decorators().size()) {
|
||||
return null;
|
||||
}
|
||||
TreeDecorator dec = tree.decorators().get(index);
|
||||
BlockData state = resolveBlock(dec.block(), dec.palette().orElse(null), blocks, pos, paletteRng);
|
||||
if (state != null && cell.facing() != null && IrisProceduralBlocks.hasProperty(state, "facing")) {
|
||||
try {
|
||||
state = blocks.withProperty(state, "facing", cell.facing().toLowerCase());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
default -> {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static BlockData finishTrunk(BlockData state, TreeBlockCanvas.Cell cell, ObjectBuilder blocks) {
|
||||
if (state == null) {
|
||||
return null;
|
||||
}
|
||||
if (cell.axis() != TreeBlockCanvas.Axis.NONE && IrisProceduralBlocks.hasProperty(state, "axis")) {
|
||||
try {
|
||||
state = blocks.withProperty(state, "axis", cell.axis().name().toLowerCase());
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
if (cell.exposed()) {
|
||||
return woodCap(state, blocks);
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
private static BlockData resolveSecondaryLeaf(Tree tree, ObjectBuilder blocks, TreeBlockCanvas.Vec pos, RNG paletteRng, RNG posRng) {
|
||||
IrisMaterialPalette secondaryPalette = tree.secondaryLeavesPalette().orElse(null);
|
||||
if (IrisProceduralBlocks.paletteSet(secondaryPalette)) {
|
||||
return blocks.pick(secondaryPalette, paletteRng.getSeed(), pos.x(), pos.y(), pos.z());
|
||||
}
|
||||
if (tree.weightedSecondaryLeaves() != null && !tree.weightedSecondaryLeaves().isEmpty()) {
|
||||
String picked = pickWeighted(tree, posRng);
|
||||
return picked == null ? null : blocks.block(picked);
|
||||
}
|
||||
if (tree.secondaryLeaves().isPresent() && !tree.secondaryLeaves().get().isEmpty()) {
|
||||
return blocks.block(tree.secondaryLeaves().get());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static String pickWeighted(Tree tree, RNG rng) {
|
||||
int total = 0;
|
||||
for (com.volmit.iris.engine.object.procedural.tree.TreeSecondaryLeaf s : tree.weightedSecondaryLeaves()) {
|
||||
total += Math.max(0, s.weight());
|
||||
}
|
||||
if (total <= 0) {
|
||||
return null;
|
||||
}
|
||||
double r = rng.nextDouble() * total;
|
||||
double cumulative = 0;
|
||||
for (com.volmit.iris.engine.object.procedural.tree.TreeSecondaryLeaf s : tree.weightedSecondaryLeaves()) {
|
||||
cumulative += Math.max(0, s.weight());
|
||||
if (r < cumulative) {
|
||||
return s.block();
|
||||
}
|
||||
}
|
||||
return tree.weightedSecondaryLeaves().get(tree.weightedSecondaryLeaves().size() - 1).block();
|
||||
}
|
||||
|
||||
private static BlockData resolveBlock(String block, IrisMaterialPalette palette, ObjectBuilder blocks, TreeBlockCanvas.Vec pos, RNG paletteRng) {
|
||||
return IrisProceduralBlocks.resolve(block, palette, blocks, pos.x(), pos.y(), pos.z(), paletteRng);
|
||||
}
|
||||
|
||||
private static BlockData woodCap(BlockData state, ObjectBuilder blocks) {
|
||||
String key = state.getAsString();
|
||||
int bracket = key.indexOf('[');
|
||||
String base = bracket < 0 ? key : key.substring(0, bracket);
|
||||
String woodKey = null;
|
||||
if (base.endsWith("_log")) {
|
||||
woodKey = base.substring(0, base.length() - 4) + "_wood";
|
||||
} else if (base.endsWith("_stem")) {
|
||||
woodKey = base.substring(0, base.length() - 5) + "_hyphae";
|
||||
}
|
||||
if (woodKey == null) {
|
||||
return state;
|
||||
}
|
||||
BlockData wood = blocks.block(woodKey);
|
||||
if (wood == null) {
|
||||
return state;
|
||||
}
|
||||
String axis = IrisProceduralBlocks.propertyValue(state, "axis");
|
||||
if (axis != null && IrisProceduralBlocks.hasProperty(wood, "axis")) {
|
||||
try {
|
||||
wood = blocks.withProperty(wood, "axis", axis);
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
}
|
||||
}
|
||||
return wood;
|
||||
}
|
||||
|
||||
private static long positionHash(TreeBlockCanvas.Vec pos) {
|
||||
return pos.x() * 73856093L ^ pos.y() * 19349663L ^ pos.z() * 83492791L;
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Controls how the chance of spawning a branch varies with trunk height (0 at the base, 1 at the top).")
|
||||
public enum TreeBranchProbability {
|
||||
@Desc("Same branch chance at every height.")
|
||||
CONSTANT,
|
||||
@Desc("Ramps straight from base chance to crown chance.")
|
||||
LINEAR,
|
||||
@Desc("S-curve that switches on around a midpoint height.")
|
||||
SIGMOID,
|
||||
@Desc("Chance rises with height raised to an exponent, clustering branches near the crown.")
|
||||
TOP_HEAVY,
|
||||
@Desc("Bell curve peaking in a height band.")
|
||||
GAUSSIAN,
|
||||
@Desc("Irregular organic branching from deterministic noise.")
|
||||
NOISE,
|
||||
@Desc("Chance falls with height (1-t)^exponent, clustering branches near the base.")
|
||||
BOTTOM_HEAVY,
|
||||
@Desc("Regular whorl rings of branches at evenly spaced heights (periods controls count).")
|
||||
PERIODIC,
|
||||
@Desc("Branches only within a height window around mean +/- std.")
|
||||
BAND,
|
||||
@Desc("Inverse of GAUSSIAN: branches avoid the mean band and favor base and crown.")
|
||||
INVERSE_GAUSSIAN,
|
||||
@Desc("Exponential decay from the base upward (constant * e^-exponent*t).")
|
||||
EXPONENTIAL_DECAY
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
import com.volmit.iris.engine.object.annotations.Snippet;
|
||||
import com.volmit.iris.engine.object.annotations.MaxNumber;
|
||||
import com.volmit.iris.engine.object.annotations.MinNumber;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Snippet("tree-branches")
|
||||
@Desc("A branch system that throws limbs out of the trunk and places a leaf ball at each tip. When present, branches drive the canopy and only the topmost crown disc of the profile is still placed.")
|
||||
public final class TreeBranches {
|
||||
@Desc("How branch spawn chance varies over trunk height.")
|
||||
private TreeBranchProbability probabilityFunction = TreeBranchProbability.TOP_HEAVY;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Branch chance for the CONSTANT probability function.")
|
||||
private double probabilityConstant = 0.5;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Branch chance at the base for the LINEAR probability function.")
|
||||
private double probabilityBase = 0;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Branch chance at the crown for the LINEAR probability function.")
|
||||
private double probabilityCrown = 1;
|
||||
|
||||
@Desc("Steepness for the SIGMOID probability function.")
|
||||
private double probabilitySteepness = 10;
|
||||
|
||||
@Desc("Midpoint height (0-1) for the SIGMOID probability function.")
|
||||
private double probabilityMidpoint = 0.7;
|
||||
|
||||
@Desc("Exponent for the TOP_HEAVY probability function.")
|
||||
private double probabilityExponent = 2;
|
||||
|
||||
@Desc("Mean height (0-1) for the GAUSSIAN probability function.")
|
||||
private double probabilityMean = 0.7;
|
||||
|
||||
@Desc("Standard deviation for the GAUSSIAN probability function.")
|
||||
private double probabilityStd = 0.15;
|
||||
|
||||
@Desc("Noise scale for the NOISE probability function.")
|
||||
private double probabilityScale = 1;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Number of whorl rings over the trunk height for the PERIODIC probability function.")
|
||||
private double probabilityPeriods = 5;
|
||||
|
||||
@Desc("How branch length varies over trunk height.")
|
||||
private TreeFunction lengthFunction = TreeFunction.LINEAR;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Branch length at the base for the LINEAR length function.")
|
||||
private double lengthBase = 1;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Branch length at the crown for the LINEAR length function.")
|
||||
private double lengthCrown = 4;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Branch length for the CONSTANT length function.")
|
||||
private double lengthConstant = 3;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Maximum branch length for the SIGMOID, LOG and PARABOLIC length functions.")
|
||||
private double lengthMax = 4;
|
||||
|
||||
@Desc("Steepness for the SIGMOID length function.")
|
||||
private double lengthSteepness = 5;
|
||||
|
||||
@Desc("How the compass direction of each branch is chosen.")
|
||||
private TreeAzimuthMode azimuthMode = TreeAzimuthMode.RANDOM;
|
||||
|
||||
@Desc("Fixed azimuth in degrees when azimuthMode is CONSTANT.")
|
||||
private double azimuth = 0;
|
||||
|
||||
@Desc("Elevation angle in degrees from horizontal. 0 is flat, positive points up, negative droops down.")
|
||||
private double elevation = 0;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("How much each branch sags/droops along its length (catenary). 0 is a straight branch; higher values arc the branch and its tip downward.")
|
||||
private double sag = 0;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(6)
|
||||
@Desc("How many recursive levels of sub-branches to grow. 0 is none, 1 is a single level, 2+ produces fractal branching.")
|
||||
private int branchDepth = 1;
|
||||
|
||||
@Desc("If true, branches are clamped to never droop below horizontal (useful for upright species).")
|
||||
private boolean leafStartUp = false;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Leaf ball radius placed at each branch tip.")
|
||||
private int clusterRadius = 2;
|
||||
|
||||
@Desc("How the branch tip leaf cluster fills.")
|
||||
private TreeLeafMode clusterMode = TreeLeafMode.TRIMMED;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Fill density for the branch tip leaf cluster when clusterMode is density or noise.")
|
||||
private double clusterDensity = 0.85;
|
||||
|
||||
@Desc("Optional one level of recursive sub-branches sprouting from each branch tip.")
|
||||
private TreeSubBranches subBranches = null;
|
||||
|
||||
public TreeBranches() {
|
||||
}
|
||||
|
||||
public TreeBranchProbability probabilityFunction() {
|
||||
return probabilityFunction == null ? TreeBranchProbability.TOP_HEAVY : probabilityFunction;
|
||||
}
|
||||
|
||||
public double probabilityConstant() {
|
||||
return probabilityConstant;
|
||||
}
|
||||
|
||||
public double probabilityBase() {
|
||||
return probabilityBase;
|
||||
}
|
||||
|
||||
public double probabilityCrown() {
|
||||
return probabilityCrown;
|
||||
}
|
||||
|
||||
public double probabilitySteepness() {
|
||||
return probabilitySteepness;
|
||||
}
|
||||
|
||||
public double probabilityMidpoint() {
|
||||
return probabilityMidpoint;
|
||||
}
|
||||
|
||||
public double probabilityExponent() {
|
||||
return probabilityExponent;
|
||||
}
|
||||
|
||||
public double probabilityMean() {
|
||||
return probabilityMean;
|
||||
}
|
||||
|
||||
public double probabilityStd() {
|
||||
return probabilityStd;
|
||||
}
|
||||
|
||||
public double probabilityScale() {
|
||||
return probabilityScale;
|
||||
}
|
||||
|
||||
public double probabilityPeriods() {
|
||||
return probabilityPeriods;
|
||||
}
|
||||
|
||||
public TreeFunction lengthFunction() {
|
||||
return lengthFunction == null ? TreeFunction.LINEAR : lengthFunction;
|
||||
}
|
||||
|
||||
public double lengthBase() {
|
||||
return lengthBase;
|
||||
}
|
||||
|
||||
public double lengthCrown() {
|
||||
return lengthCrown;
|
||||
}
|
||||
|
||||
public double lengthConstant() {
|
||||
return lengthConstant;
|
||||
}
|
||||
|
||||
public double lengthMax() {
|
||||
return lengthMax;
|
||||
}
|
||||
|
||||
public double lengthSteepness() {
|
||||
return lengthSteepness;
|
||||
}
|
||||
|
||||
public TreeAzimuthMode azimuthMode() {
|
||||
return azimuthMode == null ? TreeAzimuthMode.RANDOM : azimuthMode;
|
||||
}
|
||||
|
||||
public double azimuth() {
|
||||
return azimuth;
|
||||
}
|
||||
|
||||
public double elevation() {
|
||||
return elevation;
|
||||
}
|
||||
|
||||
public double sag() {
|
||||
return sag;
|
||||
}
|
||||
|
||||
public int branchDepth() {
|
||||
return branchDepth;
|
||||
}
|
||||
|
||||
public boolean leafStartUp() {
|
||||
return leafStartUp;
|
||||
}
|
||||
|
||||
public int clusterRadius() {
|
||||
return clusterRadius;
|
||||
}
|
||||
|
||||
public TreeLeafMode clusterMode() {
|
||||
return clusterMode == null ? TreeLeafMode.TRIMMED : clusterMode;
|
||||
}
|
||||
|
||||
public double clusterDensity() {
|
||||
return clusterDensity;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<TreeSubBranches> subBranches() {
|
||||
return Optional.ofNullable(subBranches);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
import com.volmit.iris.engine.object.annotations.Snippet;
|
||||
import com.volmit.iris.engine.object.annotations.MaxNumber;
|
||||
import com.volmit.iris.engine.object.annotations.ArrayType;
|
||||
import com.volmit.iris.engine.object.annotations.MinNumber;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Snippet("tree-canopy")
|
||||
@Desc("The leaf crown of a procedural tree. Built from stacked dome-shaped discs (driven by the profile or by explicit layers) and optionally a branch system.")
|
||||
public final class TreeCanopy {
|
||||
@MinNumber(0)
|
||||
@MaxNumber(180)
|
||||
@Desc("Elevation angle in degrees from vertical for each leaf dome. 90 is a flat disc, below 90 domes downward into a sphere, above 90 flares outward into an umbrella.")
|
||||
private double startAngle = 90;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Vertical scale of the canopy volume. Lower values flatten the crown.")
|
||||
private double squish = 1;
|
||||
|
||||
@Desc("How leaves fill each canopy disc.")
|
||||
private TreeLeafMode mode = TreeLeafMode.TRIMMED;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("Fill probability used by the density and noise leaf modes.")
|
||||
private double leafDensity = 0.85;
|
||||
|
||||
@MinNumber(0.1)
|
||||
@Desc("Horizontal X stretch of the crown for elliptical or wind-blown shapes. 1 is circular.")
|
||||
private double crownStretchX = 1;
|
||||
|
||||
@MinNumber(0.1)
|
||||
@Desc("Horizontal Z stretch of the crown for elliptical or wind-blown shapes. 1 is circular.")
|
||||
private double crownStretchZ = 1;
|
||||
|
||||
@ArrayType(min = 1, type = TreeLayer.class)
|
||||
@Desc("Optional explicit crown discs. When provided these replace the profile-driven layers and let you sculpt the silhouette by hand.")
|
||||
private List<TreeLayer> layers = List.of();
|
||||
|
||||
@Desc("Optional branch system. When present, branches build most of the canopy and only the topmost profile disc is still placed as a crown.")
|
||||
private TreeBranches branches = null;
|
||||
|
||||
public TreeCanopy() {
|
||||
}
|
||||
|
||||
public double startAngle() {
|
||||
return startAngle;
|
||||
}
|
||||
|
||||
public double squish() {
|
||||
return squish;
|
||||
}
|
||||
|
||||
public TreeLeafMode mode() {
|
||||
return mode == null ? TreeLeafMode.TRIMMED : mode;
|
||||
}
|
||||
|
||||
public double leafDensity() {
|
||||
return leafDensity;
|
||||
}
|
||||
|
||||
public double crownStretchX() {
|
||||
return crownStretchX;
|
||||
}
|
||||
|
||||
public double crownStretchZ() {
|
||||
return crownStretchZ;
|
||||
}
|
||||
|
||||
public List<TreeLayer> layers() {
|
||||
return layers == null ? List.of() : Collections.unmodifiableList(layers);
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<TreeBranches> branches() {
|
||||
return Optional.ofNullable(branches);
|
||||
}
|
||||
}
|
||||
+345
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.IrisProceduralBlocks;
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class TreeCanopyBuilder {
|
||||
private TreeCanopyBuilder() {
|
||||
}
|
||||
|
||||
public static void build(TreeBlockCanvas canvas, Tree tree, int height, double[][] offsets,
|
||||
int branchStartY, long baseSeed, List<int[]> branchEndpoints) {
|
||||
TreeCanopy canopy = tree.canopy();
|
||||
if (canopy == null) {
|
||||
canopy = new TreeCanopy();
|
||||
}
|
||||
TreeBranches branches = canopy.branches().orElse(null);
|
||||
|
||||
double[][] layers = resolveLayers(canopy, tree.profile(), height, branches != null);
|
||||
|
||||
if (branches != null) {
|
||||
if (layers.length > 0) {
|
||||
double[][] crown = new double[][]{layers[layers.length - 1]};
|
||||
volumeCanopy(canvas, tree, crown, canopy, baseSeed, offsets);
|
||||
}
|
||||
placeApexCap(canvas, tree, height, layers, offsets, baseSeed);
|
||||
branchCanopy(canvas, tree, height, branches, branchStartY, baseSeed, offsets, branchEndpoints);
|
||||
} else {
|
||||
volumeCanopy(canvas, tree, layers, canopy, baseSeed, offsets);
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeApexCap(TreeBlockCanvas canvas, Tree tree, int height, double[][] layers,
|
||||
double[][] offsets, long baseSeed) {
|
||||
int ocx = 0;
|
||||
int ocz = 0;
|
||||
if (offsets != null && offsets.length > 0) {
|
||||
ocx = (int) Math.round(offsets[offsets.length - 1][0]);
|
||||
ocz = (int) Math.round(offsets[offsets.length - 1][1]);
|
||||
}
|
||||
double maxR = 2;
|
||||
for (double[] layer : layers) {
|
||||
maxR = Math.max(maxR, layer[1]);
|
||||
}
|
||||
int capRadius = Math.max(2, Math.min(3, (int) Math.round(maxR * 0.35)));
|
||||
TreeCanopy canopy = tree.canopy();
|
||||
TreeLeafMode mode = canopy != null ? canopy.mode() : TreeLeafMode.TRIMMED;
|
||||
double density = canopy != null ? canopy.leafDensity() : 1.0;
|
||||
placeLeafCluster(canvas, tree, ocx, height - 1, ocz, capRadius, mode, density, baseSeed + 5555L);
|
||||
}
|
||||
|
||||
private static double[][] resolveLayers(TreeCanopy canopy, TreeProfile profile, int height, boolean branchDriven) {
|
||||
KList<TreeLayer> explicit = new KList<>(canopy.layers());
|
||||
if (explicit != null && !explicit.isEmpty()) {
|
||||
double[][] out = new double[explicit.size()][2];
|
||||
for (int i = 0; i < explicit.size(); i++) {
|
||||
out[i][0] = explicit.get(i).yOffset();
|
||||
out[i][1] = explicit.get(i).radius();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
return TreeProfiles.presetLayers(profile, height, branchDriven);
|
||||
}
|
||||
|
||||
private static void volumeCanopy(TreeBlockCanvas canvas, Tree tree, double[][] layers,
|
||||
TreeCanopy canopy, long baseSeed, double[][] offsets) {
|
||||
int ocx = 0;
|
||||
int ocz = 0;
|
||||
if (offsets != null && offsets.length > 0) {
|
||||
ocx = (int) Math.round(offsets[offsets.length - 1][0]);
|
||||
ocz = (int) Math.round(offsets[offsets.length - 1][1]);
|
||||
}
|
||||
|
||||
double startAngle = canopy.startAngle();
|
||||
double squish = canopy.squish();
|
||||
TreeLeafMode mode = canopy.mode();
|
||||
double leafDensity = canopy.leafDensity();
|
||||
|
||||
for (double[] layer : layers) {
|
||||
int yCenter = (int) Math.round(layer[0]);
|
||||
double radius = layer[1];
|
||||
int halfH = layerHalfHeight(radius, startAngle, squish);
|
||||
int downH = startAngle < 90.0
|
||||
? (int) Math.ceil(radius * Math.cos(Math.toRadians(startAngle)) * squish)
|
||||
: 0;
|
||||
int skirt = downH;
|
||||
|
||||
for (int y = yCenter - skirt; y <= yCenter + halfH; y++) {
|
||||
int dy = y - yCenter;
|
||||
double layerR;
|
||||
if (halfH > 0 && dy >= 0) {
|
||||
layerR = radius * Math.sqrt(Math.max(0.0, 1.0 - Math.pow(dy / (double) halfH, 2)));
|
||||
} else if (skirt > 0 && dy < 0) {
|
||||
layerR = radius * Math.sqrt(Math.max(0.0, 1.0 - Math.pow(dy / (double) skirt, 2)));
|
||||
} else {
|
||||
layerR = radius;
|
||||
}
|
||||
if (layerR < 0.5) {
|
||||
continue;
|
||||
}
|
||||
placeLeafDisc(canvas, tree, ocx, y, ocz, layerR, mode, leafDensity, baseSeed + y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int layerHalfHeight(double radius, double startAngle, double squish) {
|
||||
if (startAngle >= 180.0) {
|
||||
return 0;
|
||||
}
|
||||
return (int) Math.ceil(radius * squish);
|
||||
}
|
||||
|
||||
private static void placeLeafDisc(TreeBlockCanvas canvas, Tree tree, int cx, int cy, int cz,
|
||||
double radius, TreeLeafMode mode, double density, long seed) {
|
||||
double sx = stretchX(tree);
|
||||
double sz = stretchZ(tree);
|
||||
RNG rng = new RNG(seed);
|
||||
int irx = (int) Math.ceil(radius * sx);
|
||||
int irz = (int) Math.ceil(radius * sz);
|
||||
for (int dx = -irx; dx <= irx; dx++) {
|
||||
for (int dz = -irz; dz <= irz; dz++) {
|
||||
double ddx = dx / sx;
|
||||
double ddz = dz / sz;
|
||||
double dist = Math.sqrt(ddx * ddx + ddz * ddz);
|
||||
boolean corner = Math.abs(dx) == irx && Math.abs(dz) == irz;
|
||||
if (!passesLeafTest(mode, dist, radius, density, rng, cx + dx, cy, cz + dz, seed, corner)) {
|
||||
continue;
|
||||
}
|
||||
canvas.setLeaf(cx + dx, cy, cz + dz, resolveLeaf(tree, rng));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeLeafCluster(TreeBlockCanvas canvas, Tree tree, int cx, int cy, int cz,
|
||||
int radius, TreeLeafMode mode, double density, long seed) {
|
||||
double sx = stretchX(tree);
|
||||
double sz = stretchZ(tree);
|
||||
RNG rng = new RNG(seed);
|
||||
int irx = (int) Math.ceil(radius * sx);
|
||||
int irz = (int) Math.ceil(radius * sz);
|
||||
for (int dx = -irx; dx <= irx; dx++) {
|
||||
for (int dy = -radius; dy <= radius; dy++) {
|
||||
for (int dz = -irz; dz <= irz; dz++) {
|
||||
double ddx = dx / sx;
|
||||
double ddz = dz / sz;
|
||||
double dist = Math.sqrt(ddx * ddx + dy * dy + ddz * ddz);
|
||||
boolean corner = Math.abs(dx) == irx && Math.abs(dz) == irz;
|
||||
if (!passesLeafTest(mode, dist, radius, density, rng, cx + dx, cy + dy, cz + dz, seed, corner)) {
|
||||
continue;
|
||||
}
|
||||
canvas.setLeaf(cx + dx, cy + dy, cz + dz, resolveLeaf(tree, rng));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static double stretchX(Tree tree) {
|
||||
TreeCanopy c = tree.canopy();
|
||||
return c == null ? 1.0 : Math.max(0.1, c.crownStretchX());
|
||||
}
|
||||
|
||||
private static double stretchZ(Tree tree) {
|
||||
TreeCanopy c = tree.canopy();
|
||||
return c == null ? 1.0 : Math.max(0.1, c.crownStretchZ());
|
||||
}
|
||||
|
||||
private static boolean passesLeafTest(TreeLeafMode mode, double dist, double radius, double density,
|
||||
RNG rng, int wx, int wy, int wz, long seed, boolean corner) {
|
||||
switch (mode) {
|
||||
case TRIMMED -> {
|
||||
return !(dist > radius || corner);
|
||||
}
|
||||
case FILLED -> {
|
||||
return dist <= radius;
|
||||
}
|
||||
case DENSITY -> {
|
||||
if (dist > radius) {
|
||||
return false;
|
||||
}
|
||||
return rng.nextDouble() <= density * (1.0 - (dist / (radius + 1.0)));
|
||||
}
|
||||
case NOISE -> {
|
||||
if (dist > radius) {
|
||||
return false;
|
||||
}
|
||||
return TreeFunctions.valueNoise3D(wx, wy, wz, seed) <= density;
|
||||
}
|
||||
case HOLLOW -> {
|
||||
return dist <= radius && dist > radius - 1.0;
|
||||
}
|
||||
case GRADIENT -> {
|
||||
if (dist > radius) {
|
||||
return false;
|
||||
}
|
||||
return rng.nextDouble() <= density * Math.pow(1.0 - (dist / (radius + 1.0)), 2);
|
||||
}
|
||||
case CLUMPED -> {
|
||||
if (dist > radius) {
|
||||
return false;
|
||||
}
|
||||
return TreeFunctions.valueNoise3D(Math.floorDiv(wx, 2), Math.floorDiv(wy, 2), Math.floorDiv(wz, 2), seed) <= density;
|
||||
}
|
||||
case TATTERED -> {
|
||||
if (dist > radius || corner) {
|
||||
return false;
|
||||
}
|
||||
return !(dist > radius - 1.5 && rng.nextDouble() < 0.6);
|
||||
}
|
||||
case SPARSE -> {
|
||||
if (dist > radius) {
|
||||
return false;
|
||||
}
|
||||
return rng.nextDouble() <= density * 0.4;
|
||||
}
|
||||
default -> {
|
||||
return dist <= radius;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static TreeBlockCanvas.Role resolveLeaf(Tree tree, RNG rng) {
|
||||
boolean hasSecondary = IrisProceduralBlocks.paletteSet(tree.secondaryLeavesPalette())
|
||||
|| (tree.weightedSecondaryLeaves() != null && !tree.weightedSecondaryLeaves().isEmpty())
|
||||
|| tree.secondaryLeaves().filter(leaves -> !leaves.isEmpty()).isPresent();
|
||||
if (!hasSecondary || tree.secondaryLeafFraction() <= 0.0) {
|
||||
return TreeBlockCanvas.Role.LEAF;
|
||||
}
|
||||
if (rng.nextDouble() >= tree.secondaryLeafFraction()) {
|
||||
return TreeBlockCanvas.Role.LEAF;
|
||||
}
|
||||
return TreeBlockCanvas.Role.SECONDARY_LEAF;
|
||||
}
|
||||
|
||||
private static void branchCanopy(TreeBlockCanvas canvas, Tree tree, int height,
|
||||
TreeBranches branches, int branchStartY, long baseSeed, double[][] offsets,
|
||||
List<int[]> branchEndpoints) {
|
||||
long branchSeed = baseSeed + 9999;
|
||||
RNG branchRng = new RNG(branchSeed);
|
||||
TreeSubBranches sub = branches.subBranches().orElse(null);
|
||||
int depth = branches.branchDepth();
|
||||
int branchIndex = 0;
|
||||
|
||||
for (int y = Math.max(0, branchStartY); y < height; y++) {
|
||||
double t = y / (double) Math.max(height - 1, 1);
|
||||
double p = TreeFunctions.branchProbability(branches, t, branchSeed);
|
||||
if (branchRng.nextDouble() > p) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int ox = 0;
|
||||
int oz = 0;
|
||||
if (offsets != null && y < offsets.length) {
|
||||
ox = (int) Math.round(offsets[y][0]);
|
||||
oz = (int) Math.round(offsets[y][1]);
|
||||
}
|
||||
|
||||
double az = TreeFunctions.azimuthDegrees(branches.azimuthMode(), t, branchIndex, tree, branches.azimuth(), branchRng);
|
||||
branchIndex++;
|
||||
double branchLen = TreeFunctions.branchLength(branches, t);
|
||||
double effElevation = branches.leafStartUp() ? Math.max(0.0, branches.elevation()) : branches.elevation();
|
||||
int[] end = branchEndpoint(ox, y, oz, az, effElevation, branchLen);
|
||||
int[] tip = rasterizeBranch(canvas, ox, y, oz, end[0], end[1], end[2], branches.sag());
|
||||
|
||||
if (branchEndpoints != null) {
|
||||
branchEndpoints.add(new int[]{tip[0], tip[1], tip[2], ox, oz});
|
||||
}
|
||||
placeLeafCluster(canvas, tree, tip[0], tip[1], tip[2], branches.clusterRadius(),
|
||||
branches.clusterMode(), branches.clusterDensity(), branchSeed + y);
|
||||
|
||||
growSubBranches(canvas, tree, tip[0], tip[1], tip[2], az, effElevation, branchLen, sub, depth, branchSeed + y + 1000L);
|
||||
}
|
||||
}
|
||||
|
||||
private static void growSubBranches(TreeBlockCanvas canvas, Tree tree, int ox, int oy, int oz,
|
||||
double az, double el, double len, TreeSubBranches sub, int depth, long seed) {
|
||||
if (sub == null || depth <= 0) {
|
||||
return;
|
||||
}
|
||||
int count = Math.max(1, sub.count());
|
||||
for (int si = 0; si < count; si++) {
|
||||
double yawOffset = sub.yawDelta() * (si - (count - 1) / 2.0);
|
||||
double subAz = az + yawOffset;
|
||||
double subEl = el + sub.pitchDelta();
|
||||
double subLen = len * sub.lengthScale();
|
||||
int[] end = branchEndpoint(ox, oy, oz, subAz, subEl, subLen);
|
||||
int[] tip = rasterizeBranch(canvas, ox, oy, oz, end[0], end[1], end[2], sub.sag());
|
||||
placeLeafCluster(canvas, tree, tip[0], tip[1], tip[2], sub.clusterRadius(),
|
||||
sub.clusterMode(), sub.clusterDensity(), seed + si * 131L);
|
||||
growSubBranches(canvas, tree, tip[0], tip[1], tip[2], subAz, subEl, subLen, sub, depth - 1, seed + si * 131L + 17L);
|
||||
}
|
||||
}
|
||||
|
||||
private static int[] branchEndpoint(int ox, int oy, int oz, double azimuthDeg, double elevationDeg, double length) {
|
||||
double azRad = Math.toRadians(azimuthDeg);
|
||||
double elRad = Math.toRadians(elevationDeg);
|
||||
double dx = length * Math.cos(elRad) * Math.sin(azRad);
|
||||
double dy = length * Math.sin(elRad);
|
||||
double dz = length * Math.cos(elRad) * Math.cos(azRad);
|
||||
return new int[]{ox + (int) Math.round(dx), oy + (int) Math.round(dy), oz + (int) Math.round(dz)};
|
||||
}
|
||||
|
||||
private static int[] rasterizeBranch(TreeBlockCanvas canvas, int ox, int oy, int oz, int ex, int ey, int ez, double sag) {
|
||||
int dx = ex - ox;
|
||||
int dy = ey - oy;
|
||||
int dz = ez - oz;
|
||||
double horizontal = Math.sqrt(dx * dx + dz * dz);
|
||||
double sagBlocks = sag * horizontal;
|
||||
int steps = Math.max(Math.max(Math.abs(dx), Math.abs(dz)),
|
||||
Math.max((int) (Math.abs(dy) + Math.ceil(sagBlocks)), 1));
|
||||
TreeBlockCanvas.Axis axis = TreeFunctions.logAxis(dx, dy, dz);
|
||||
|
||||
int[] tip = new int[]{ox, oy, oz};
|
||||
for (int i = 0; i <= steps; i++) {
|
||||
double t = i / (double) steps;
|
||||
int x = (int) Math.round(ox + dx * t);
|
||||
int y = (int) Math.round(oy + dy * t - sagBlocks * t * t);
|
||||
int z = (int) Math.round(oz + dz * t);
|
||||
if (!canvas.has(x, y, z)) {
|
||||
canvas.setTrunk(x, y, z, TreeBlockCanvas.Role.TRUNK, axis);
|
||||
}
|
||||
tip = new int[]{x, y, z};
|
||||
}
|
||||
return tip;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.IrisMaterialPalette;
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
import com.volmit.iris.engine.object.annotations.Snippet;
|
||||
import com.volmit.iris.engine.object.annotations.MaxNumber;
|
||||
import com.volmit.iris.engine.object.annotations.MinNumber;
|
||||
import com.volmit.iris.engine.object.annotations.RegistryListBlockType;
|
||||
import com.volmit.iris.engine.object.annotations.Required;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Snippet("tree-decorator")
|
||||
@Desc("An accent block applied to a generated tree such as fruit on branch tips, vines on the trunk, snow on the crown, or a glowing underside.")
|
||||
public final class TreeDecorator {
|
||||
@Desc("Where this decorator is placed on the tree.")
|
||||
private TreeDecoratorTarget target = TreeDecoratorTarget.BRANCH_TIP;
|
||||
|
||||
@Required
|
||||
@Desc("The block id to place, e.g. minecraft:magma_block. Ignored when palette is set.")
|
||||
@RegistryListBlockType
|
||||
private String block = "";
|
||||
|
||||
@Desc("A noise-driven palette for this decorator. When set this overrides the single block.")
|
||||
private IrisMaterialPalette palette;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("The chance per candidate position for this decorator to place.")
|
||||
private double chance = 0.5;
|
||||
|
||||
@MinNumber(1)
|
||||
@Desc("Maximum strand length downward for the CANOPY_HANG target (random 1..length per column).")
|
||||
private int length = 1;
|
||||
|
||||
@Desc("If true the block's facing is oriented away from the trunk (for trunk-mounted fences/gates/banners).")
|
||||
private boolean axisAware = false;
|
||||
|
||||
public TreeDecorator() {
|
||||
}
|
||||
|
||||
public TreeDecoratorTarget target() {
|
||||
return target == null ? TreeDecoratorTarget.BRANCH_TIP : target;
|
||||
}
|
||||
|
||||
public String block() {
|
||||
return block;
|
||||
}
|
||||
|
||||
/** Empty where the pack sets none. */
|
||||
public Optional<IrisMaterialPalette> palette() {
|
||||
return Optional.ofNullable(palette);
|
||||
}
|
||||
|
||||
public double chance() {
|
||||
return chance;
|
||||
}
|
||||
|
||||
public int length() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public boolean axisAware() {
|
||||
return axisAware;
|
||||
}
|
||||
}
|
||||
+278
@@ -0,0 +1,278 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.util.collection.KList;
|
||||
import com.volmit.iris.util.collection.KMap;
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class TreeDecoratorApplier {
|
||||
private TreeDecoratorApplier() {
|
||||
}
|
||||
|
||||
public static void apply(TreeBlockCanvas canvas, Tree tree, long baseSeed, List<int[]> branchEndpoints) {
|
||||
List<TreeDecorator> decorators = tree.decorators();
|
||||
if (decorators == null || decorators.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
RNG rng = new RNG(baseSeed + 77777L);
|
||||
|
||||
for (int idx = 0; idx < decorators.size(); idx++) {
|
||||
TreeDecorator dec = decorators.get(idx);
|
||||
boolean hasBlock = (dec.block() != null && !dec.block().isEmpty()) || dec.palette().isPresent();
|
||||
if (!hasBlock) {
|
||||
continue;
|
||||
}
|
||||
switch (dec.target()) {
|
||||
case BRANCH_TIP -> branchTip(canvas, dec, idx, branchEndpoints, rng);
|
||||
case TRUNK_SURFACE -> trunkSurface(canvas, dec, idx, rng);
|
||||
case CANOPY_TOP -> canopyTop(canvas, dec, idx, rng);
|
||||
case CANOPY_BOTTOM -> canopyBottom(canvas, dec, idx, rng);
|
||||
case TRUNK_BASE -> trunkBase(canvas, dec, idx, rng);
|
||||
case LEAF_SURFACE -> leafSurface(canvas, dec, idx, rng);
|
||||
case CANOPY_HANG -> canopyHang(canvas, dec, idx, rng);
|
||||
case BRANCH_SURFACE -> branchSurface(canvas, dec, idx, rng);
|
||||
case TRUNK_TOP -> trunkTop(canvas, dec, idx, rng);
|
||||
case GROUND_SCATTER -> groundScatter(canvas, dec, idx, rng);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void branchTip(TreeBlockCanvas canvas, TreeDecorator dec, int idx, List<int[]> branchEndpoints, RNG rng) {
|
||||
if (branchEndpoints == null) {
|
||||
return;
|
||||
}
|
||||
for (int[] e : branchEndpoints) {
|
||||
if (rng.nextDouble() > dec.chance()) {
|
||||
continue;
|
||||
}
|
||||
if (canvas.has(e[0], e[1], e[2])) {
|
||||
continue;
|
||||
}
|
||||
String facing = dec.axisAware() ? facingAway(e[3], e[4], e[0], e[2]) : null;
|
||||
canvas.setDecor(e[0], e[1], e[2], idx, facing);
|
||||
}
|
||||
}
|
||||
|
||||
private static void trunkSurface(TreeBlockCanvas canvas, TreeDecorator dec, int idx, RNG rng) {
|
||||
int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
String[] facings = {"west", "east", "north", "south"};
|
||||
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) {
|
||||
for (int i = 0; i < sides.length; i++) {
|
||||
int nx = v.x() + sides[i][0];
|
||||
int nz = v.z() + sides[i][1];
|
||||
if (canvas.has(nx, v.y(), nz) || rng.nextDouble() > dec.chance()) {
|
||||
continue;
|
||||
}
|
||||
canvas.setDecor(nx, v.y(), nz, idx, dec.axisAware() ? facings[i] : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void leafSurface(TreeBlockCanvas canvas, TreeDecorator dec, int idx, RNG rng) {
|
||||
int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
String[] facings = {"west", "east", "north", "south"};
|
||||
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getLeaf())) {
|
||||
for (int i = 0; i < sides.length; i++) {
|
||||
int nx = v.x() + sides[i][0];
|
||||
int nz = v.z() + sides[i][1];
|
||||
if (canvas.has(nx, v.y(), nz) || rng.nextDouble() > dec.chance()) {
|
||||
continue;
|
||||
}
|
||||
canvas.setDecor(nx, v.y(), nz, idx, dec.axisAware() ? facings[i] : null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void branchSurface(TreeBlockCanvas canvas, TreeDecorator dec, int idx, RNG rng) {
|
||||
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) {
|
||||
int ay = v.y() + 1;
|
||||
if (canvas.has(v.x(), ay, v.z()) || rng.nextDouble() > dec.chance()) {
|
||||
continue;
|
||||
}
|
||||
canvas.setDecor(v.x(), ay, v.z(), idx, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void canopyTop(TreeBlockCanvas canvas, TreeDecorator dec, int idx, RNG rng) {
|
||||
KMap<Long, Integer> colTop = new KMap<>();
|
||||
for (TreeBlockCanvas.Vec v : canvas.getCells().keySet()) {
|
||||
long key = pack(v.x(), v.z());
|
||||
Integer cur = colTop.get(key);
|
||||
if (cur == null || v.y() > cur) {
|
||||
colTop.put(key, v.y());
|
||||
}
|
||||
}
|
||||
for (Map.Entry<Long, Integer> e : colTop.entrySet()) {
|
||||
int x = unpackX(e.getKey());
|
||||
int z = unpackZ(e.getKey());
|
||||
int aboveY = e.getValue() + 1;
|
||||
if (canvas.has(x, aboveY, z) || rng.nextDouble() > dec.chance()) {
|
||||
continue;
|
||||
}
|
||||
canvas.setDecor(x, aboveY, z, idx, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void canopyBottom(TreeBlockCanvas canvas, TreeDecorator dec, int idx, RNG rng) {
|
||||
KMap<Long, Integer> colBottom = columnLeafBottoms(canvas);
|
||||
for (Map.Entry<Long, Integer> e : colBottom.entrySet()) {
|
||||
int x = unpackX(e.getKey());
|
||||
int z = unpackZ(e.getKey());
|
||||
int belowY = e.getValue() - 1;
|
||||
if (canvas.has(x, belowY, z) || rng.nextDouble() > dec.chance()) {
|
||||
continue;
|
||||
}
|
||||
canvas.setDecor(x, belowY, z, idx, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void canopyHang(TreeBlockCanvas canvas, TreeDecorator dec, int idx, RNG rng) {
|
||||
int maxLen = Math.max(1, dec.length());
|
||||
KMap<Long, Integer> colBottom = columnLeafBottoms(canvas);
|
||||
for (Map.Entry<Long, Integer> e : colBottom.entrySet()) {
|
||||
if (rng.nextDouble() > dec.chance()) {
|
||||
continue;
|
||||
}
|
||||
int x = unpackX(e.getKey());
|
||||
int z = unpackZ(e.getKey());
|
||||
int strand = rng.i(1, maxLen + 1);
|
||||
for (int k = 1; k <= strand; k++) {
|
||||
int yy = e.getValue() - k;
|
||||
if (canvas.has(x, yy, z)) {
|
||||
break;
|
||||
}
|
||||
canvas.setDecor(x, yy, z, idx, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void trunkBase(TreeBlockCanvas canvas, TreeDecorator dec, int idx, RNG rng) {
|
||||
int[][] sides = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
|
||||
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) {
|
||||
if (v.y() != 0) {
|
||||
continue;
|
||||
}
|
||||
for (int[] side : sides) {
|
||||
int nx = v.x() + side[0];
|
||||
int nz = v.z() + side[1];
|
||||
if (canvas.has(nx, 0, nz) || rng.nextDouble() > dec.chance()) {
|
||||
continue;
|
||||
}
|
||||
canvas.setDecor(nx, 0, nz, idx, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void trunkTop(TreeBlockCanvas canvas, TreeDecorator dec, int idx, RNG rng) {
|
||||
int maxY = Integer.MIN_VALUE;
|
||||
for (TreeBlockCanvas.Vec v : canvas.getTrunk()) {
|
||||
maxY = Math.max(maxY, v.y());
|
||||
}
|
||||
if (maxY == Integer.MIN_VALUE) {
|
||||
return;
|
||||
}
|
||||
for (TreeBlockCanvas.Vec v : new KList<>(canvas.getTrunk())) {
|
||||
if (v.y() != maxY) {
|
||||
continue;
|
||||
}
|
||||
int aboveY = v.y() + 1;
|
||||
if (canvas.has(v.x(), aboveY, v.z()) || rng.nextDouble() > dec.chance()) {
|
||||
continue;
|
||||
}
|
||||
canvas.setDecor(v.x(), aboveY, v.z(), idx, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void groundScatter(TreeBlockCanvas canvas, TreeDecorator dec, int idx, RNG rng) {
|
||||
double cx = 0;
|
||||
double cz = 0;
|
||||
int count = 0;
|
||||
double footprint = 1.0;
|
||||
for (TreeBlockCanvas.Vec v : canvas.getTrunk()) {
|
||||
if (v.y() == 0) {
|
||||
cx += v.x();
|
||||
cz += v.z();
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count == 0) {
|
||||
return;
|
||||
}
|
||||
cx /= count;
|
||||
cz /= count;
|
||||
for (TreeBlockCanvas.Vec v : canvas.getTrunk()) {
|
||||
if (v.y() == 0) {
|
||||
footprint = Math.max(footprint, Math.hypot(v.x() - cx, v.z() - cz));
|
||||
}
|
||||
}
|
||||
int radius = (int) Math.ceil(footprint + 3.0);
|
||||
int icx = (int) Math.round(cx);
|
||||
int icz = (int) Math.round(cz);
|
||||
for (int dx = -radius; dx <= radius; dx++) {
|
||||
for (int dz = -radius; dz <= radius; dz++) {
|
||||
if (Math.hypot(dx, dz) > radius) {
|
||||
continue;
|
||||
}
|
||||
int x = icx + dx;
|
||||
int z = icz + dz;
|
||||
if (canvas.has(x, 0, z) || rng.nextDouble() > dec.chance()) {
|
||||
continue;
|
||||
}
|
||||
canvas.setDecor(x, 0, z, idx, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static KMap<Long, Integer> columnLeafBottoms(TreeBlockCanvas canvas) {
|
||||
KMap<Long, Integer> colBottom = new KMap<>();
|
||||
for (TreeBlockCanvas.Vec v : canvas.getLeaf()) {
|
||||
long key = pack(v.x(), v.z());
|
||||
Integer cur = colBottom.get(key);
|
||||
if (cur == null || v.y() < cur) {
|
||||
colBottom.put(key, v.y());
|
||||
}
|
||||
}
|
||||
return colBottom;
|
||||
}
|
||||
|
||||
private static String facingAway(int cx, int cz, int x, int z) {
|
||||
int dx = x - cx;
|
||||
int dz = z - cz;
|
||||
if (Math.abs(dx) >= Math.abs(dz)) {
|
||||
return dx >= 0 ? "east" : "west";
|
||||
}
|
||||
return dz >= 0 ? "south" : "north";
|
||||
}
|
||||
|
||||
private static long pack(int x, int z) {
|
||||
return (((long) x) << 32) ^ (z & 0xFFFFFFFFL);
|
||||
}
|
||||
|
||||
private static int unpackX(long key) {
|
||||
return (int) (key >> 32);
|
||||
}
|
||||
|
||||
private static int unpackZ(long key) {
|
||||
return (int) (key & 0xFFFFFFFFL);
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Where a tree decorator block is placed.")
|
||||
public enum TreeDecoratorTarget {
|
||||
@Desc("At the tip of each generated branch (vines, fruit, lanterns).")
|
||||
BRANCH_TIP,
|
||||
@Desc("On air-facing sides of trunk and branch blocks (vines, moss).")
|
||||
TRUNK_SURFACE,
|
||||
@Desc("On top of the highest block in each column (snow, sculk).")
|
||||
CANOPY_TOP,
|
||||
@Desc("One block below the lowest leaf in each column (glowing underside).")
|
||||
CANOPY_BOTTOM,
|
||||
@Desc("Around the y=0 ring at the base of the trunk (roots, mushrooms).")
|
||||
TRUNK_BASE,
|
||||
@Desc("On air-facing sides of leaf blocks (berries, lights nestled in the foliage).")
|
||||
LEAF_SURFACE,
|
||||
@Desc("Hanging strands of up to 'length' blocks below the lowest leaf in each column (vines, icicles).")
|
||||
CANOPY_HANG,
|
||||
@Desc("On the top face of trunk and branch blocks (moss, snow on limbs).")
|
||||
BRANCH_SURFACE,
|
||||
@Desc("One block above the very top of the trunk (nest, beehive, beacon).")
|
||||
TRUNK_TOP,
|
||||
@Desc("Scattered across the ground footprint around the base, wider than the trunk-base ring.")
|
||||
GROUND_SCATTER
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("A shaping function evaluated over normalized height (0 at the base, 1 at the top). Used for trunk width, trunk curve and branch length.")
|
||||
public enum TreeFunction {
|
||||
@Desc("Same value at every height.")
|
||||
CONSTANT,
|
||||
@Desc("Ramps straight from start to end.")
|
||||
LINEAR,
|
||||
@Desc("S-curve transition controlled by steepness.")
|
||||
SIGMOID,
|
||||
@Desc("Logarithmic falloff controlled by base.")
|
||||
LOG,
|
||||
@Desc("Sine ripple controlled by period and amplitude.")
|
||||
SINE,
|
||||
@Desc("Parabolic pinch, narrowest at peakOffset.")
|
||||
PARABOLIC,
|
||||
@Desc("Geometric (exponential) interpolation from start to end.")
|
||||
EXPONENTIAL,
|
||||
@Desc("Square-root ease: fast change low, slow high.")
|
||||
SQRT,
|
||||
@Desc("Hard step that jumps from start to end at the threshold (peakOffset).")
|
||||
STEP,
|
||||
@Desc("Bell bulge peaking in the middle of the height range.")
|
||||
BELL,
|
||||
@Desc("Smoothstep ease-in-out (3t^2 - 2t^3).")
|
||||
EASE_IN_OUT
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
public final class TreeFunctions {
|
||||
private static final double GOLDEN_ANGLE = 137.50776405003785;
|
||||
|
||||
private TreeFunctions() {
|
||||
}
|
||||
|
||||
public static double trunkWidthMultiplier(Tree tree, double t) {
|
||||
double start = tree.shapeStart();
|
||||
double end = tree.shapeEnd();
|
||||
return switch (tree.trunkShape()) {
|
||||
case CONSTANT -> 1.0;
|
||||
case LINEAR -> start + (end - start) * t;
|
||||
case SIGMOID -> 1.0 / (1.0 + Math.exp(tree.shapeSteepness() * (t - 0.5)));
|
||||
case LOG -> {
|
||||
double base = Math.max(1.0001, tree.shapeBase());
|
||||
if (t <= 0.0) {
|
||||
yield 1.0;
|
||||
}
|
||||
yield Math.max(0.0, 1.0 - Math.log(1.0 + t * (base - 1.0)) / Math.log(base));
|
||||
}
|
||||
case SINE -> {
|
||||
double period = tree.shapePeriod() == 0 ? 1.0 : tree.shapePeriod();
|
||||
yield 1.0 + tree.shapeAmplitude() * Math.sin(2.0 * Math.PI * t / period);
|
||||
}
|
||||
case PARABOLIC -> {
|
||||
double peak = tree.shapePeakOffset();
|
||||
double floor = tree.shapeFloor();
|
||||
double denom = Math.pow(Math.max(peak, 1.0 - peak), 2);
|
||||
if (denom == 0) {
|
||||
denom = 1;
|
||||
}
|
||||
yield floor + (1.0 - floor) * (Math.pow(t - peak, 2) / denom);
|
||||
}
|
||||
case EXPONENTIAL -> geometric(start, end, t);
|
||||
case SQRT -> start + (end - start) * Math.sqrt(t);
|
||||
case STEP -> t < tree.shapePeakOffset() ? start : end;
|
||||
case BELL -> start + (end - start) * bell(t);
|
||||
case EASE_IN_OUT -> start + (end - start) * smoothstep(t);
|
||||
};
|
||||
}
|
||||
|
||||
public static double curveProgress(TreeFunction fn, double t, double steepness) {
|
||||
return switch (fn) {
|
||||
case LINEAR, CONSTANT, SINE -> t;
|
||||
case LOG -> Math.log(1.0 + t * (Math.E - 1.0));
|
||||
case SIGMOID -> 1.0 / (1.0 + Math.exp(-steepness * (t - 0.5)));
|
||||
case PARABOLIC, EASE_IN_OUT -> smoothstep(t);
|
||||
case EXPONENTIAL -> Math.pow(t, Math.max(1.0, steepness));
|
||||
case SQRT -> Math.sqrt(t);
|
||||
case STEP -> t < 0.5 ? 0.0 : 1.0;
|
||||
case BELL -> bell(t);
|
||||
};
|
||||
}
|
||||
|
||||
public static double branchLength(TreeBranches b, double t) {
|
||||
double base = b.lengthBase();
|
||||
double crown = b.lengthCrown();
|
||||
double max = b.lengthMax();
|
||||
double v = switch (b.lengthFunction()) {
|
||||
case CONSTANT -> b.lengthConstant();
|
||||
case LINEAR, SINE -> base + (crown - base) * t;
|
||||
case SIGMOID -> max / (1.0 + Math.exp(-b.lengthSteepness() * (t - 0.5)));
|
||||
case LOG -> max * Math.log(1.0 + t * (Math.E - 1.0));
|
||||
case PARABOLIC -> max * (1.0 - Math.pow(2.0 * t - 1.0, 2));
|
||||
case EXPONENTIAL -> geometric(base, crown, t);
|
||||
case SQRT -> base + (crown - base) * Math.sqrt(t);
|
||||
case STEP -> t < 0.5 ? base : crown;
|
||||
case BELL -> base + (max - base) * bell(t);
|
||||
case EASE_IN_OUT -> base + (crown - base) * smoothstep(t);
|
||||
};
|
||||
return Math.max(1.0, v);
|
||||
}
|
||||
|
||||
public static double branchProbability(TreeBranches b, double t, long seed) {
|
||||
double std = Math.max(b.probabilityStd(), 1e-6);
|
||||
return switch (b.probabilityFunction()) {
|
||||
case CONSTANT -> b.probabilityConstant();
|
||||
case LINEAR -> b.probabilityBase() + (b.probabilityCrown() - b.probabilityBase()) * t;
|
||||
case SIGMOID -> 1.0 / (1.0 + Math.exp(-b.probabilitySteepness() * (t - b.probabilityMidpoint())));
|
||||
case TOP_HEAVY -> Math.pow(t, b.probabilityExponent());
|
||||
case GAUSSIAN -> Math.exp(-0.5 * Math.pow((t - b.probabilityMean()) / std, 2));
|
||||
case NOISE -> valueNoise1D(t * b.probabilityScale(), seed);
|
||||
case BOTTOM_HEAVY -> Math.pow(1.0 - t, b.probabilityExponent());
|
||||
case PERIODIC -> b.probabilityConstant() * Math.pow(Math.max(0.0, Math.sin(2.0 * Math.PI * t * b.probabilityPeriods())), 8);
|
||||
case BAND -> Math.abs(t - b.probabilityMean()) <= b.probabilityStd() ? b.probabilityConstant() : 0.0;
|
||||
case INVERSE_GAUSSIAN -> b.probabilityConstant() * (1.0 - Math.exp(-0.5 * Math.pow((t - b.probabilityMean()) / std, 2)));
|
||||
case EXPONENTIAL_DECAY -> b.probabilityConstant() * Math.exp(-b.probabilityExponent() * t);
|
||||
};
|
||||
}
|
||||
|
||||
public static double azimuthDegrees(TreeAzimuthMode mode, double t, int index, Tree tree, double constantBase, RNG rng) {
|
||||
return switch (mode) {
|
||||
case CONSTANT -> constantBase;
|
||||
case LINEAR -> tree.azimuthStart() + (tree.azimuthEnd() - tree.azimuthStart()) * t;
|
||||
case SPIRAL -> tree.azimuthStart() + tree.azimuthTurns() * 360.0 * t;
|
||||
case SINE -> {
|
||||
double period = tree.azimuthPeriod() == 0 ? 1.0 : tree.azimuthPeriod();
|
||||
yield tree.azimuthOffset() + tree.azimuthAmplitude() * Math.sin(2.0 * Math.PI * t / period);
|
||||
}
|
||||
case NOISE -> noiseAzimuth(t, tree.azimuthScale(), tree.seed());
|
||||
case RANDOM -> rng != null ? rng.d(0.0, 360.0) : constantBase;
|
||||
case GOLDEN_ANGLE -> index >= 0 ? constantBase + index * GOLDEN_ANGLE : constantBase;
|
||||
case ALTERNATING -> index >= 0 ? constantBase + (index % 2) * 180.0 : constantBase;
|
||||
case WHORL -> {
|
||||
int count = Math.max(1, tree.azimuthWhorlCount());
|
||||
yield index >= 0 ? constantBase + (index % count) * (360.0 / count) : constantBase;
|
||||
}
|
||||
case ZIGZAG -> index >= 0 ? constantBase + ((index % 2 == 0) ? -tree.azimuthAmplitude() : tree.azimuthAmplitude()) : constantBase;
|
||||
};
|
||||
}
|
||||
|
||||
public static TreeBlockCanvas.Axis logAxis(double dx, double dy, double dz) {
|
||||
double ax = Math.abs(dx);
|
||||
double ay = Math.abs(dy) * 1.8;
|
||||
double az = Math.abs(dz);
|
||||
if (ay >= ax && ay >= az) {
|
||||
return TreeBlockCanvas.Axis.Y;
|
||||
}
|
||||
if (ax >= az) {
|
||||
return TreeBlockCanvas.Axis.X;
|
||||
}
|
||||
return TreeBlockCanvas.Axis.Z;
|
||||
}
|
||||
|
||||
private static double geometric(double a, double b, double t) {
|
||||
if (a <= 0.0 || b <= 0.0) {
|
||||
return a + (b - a) * t;
|
||||
}
|
||||
return a * Math.pow(b / a, t);
|
||||
}
|
||||
|
||||
private static double bell(double t) {
|
||||
return Math.exp(-Math.pow(t - 0.5, 2) / (2.0 * 0.15 * 0.15));
|
||||
}
|
||||
|
||||
private static double smoothstep(double t) {
|
||||
return 3.0 * t * t - 2.0 * t * t * t;
|
||||
}
|
||||
|
||||
public static double noiseAzimuth(double t, double scale, long seed) {
|
||||
double a = valueNoise1D(t * scale, seed);
|
||||
double b = valueNoise1D(t * scale * 0.5, seed + 1);
|
||||
return ((a + b) / 2.0) * 360.0;
|
||||
}
|
||||
|
||||
public static double valueNoise1D(double x, long seed) {
|
||||
long h = mix(seed * 0x9E3779B97F4A7C15L + Math.round(x * 1000.0));
|
||||
return unit(h);
|
||||
}
|
||||
|
||||
public static double valueNoise3D(int x, int y, int z, long seed) {
|
||||
long h = mix(seed);
|
||||
h = mix(h + x * 0x9E3779B97F4A7C15L);
|
||||
h = mix(h + y * 0xC2B2AE3D27D4EB4FL);
|
||||
h = mix(h + z * 0x165667B19E3779F9L);
|
||||
return unit(h);
|
||||
}
|
||||
|
||||
private static long mix(long a) {
|
||||
a ^= (a >>> 33);
|
||||
a *= 0xff51afd7ed558ccdL;
|
||||
a ^= (a >>> 33);
|
||||
a *= 0xc4ceb9fe1a85ec53L;
|
||||
a ^= (a >>> 33);
|
||||
return a;
|
||||
}
|
||||
|
||||
private static double unit(long h) {
|
||||
return (h >>> 11) * 0x1.0p-53;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
import com.volmit.iris.engine.object.annotations.Snippet;
|
||||
import com.volmit.iris.engine.object.annotations.MinNumber;
|
||||
|
||||
@Snippet("tree-layer")
|
||||
@Desc("An explicit canopy disc at a given vertical offset and radius. Define a list of these to bypass the profile and sculpt the crown by hand.")
|
||||
public final class TreeLayer {
|
||||
@Desc("The vertical offset of this disc, measured in blocks above the trunk base.")
|
||||
private int yOffset = 0;
|
||||
|
||||
@MinNumber(0.5)
|
||||
@Desc("The radius of this leaf disc in blocks.")
|
||||
private double radius = 2;
|
||||
|
||||
public TreeLayer() {
|
||||
}
|
||||
|
||||
public int yOffset() {
|
||||
return yOffset;
|
||||
}
|
||||
|
||||
public double radius() {
|
||||
return radius;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("How leaves fill a disc or cluster.")
|
||||
public enum TreeLeafMode {
|
||||
@Desc("Corner-trimmed disc/sphere for a rounded silhouette.")
|
||||
TRIMMED,
|
||||
@Desc("Completely filled disc/sphere.")
|
||||
FILLED,
|
||||
@Desc("Seeded probabilistic falloff toward the edge controlled by leafDensity.")
|
||||
DENSITY,
|
||||
@Desc("Seeded value-noise mask controlled by leafDensity.")
|
||||
NOISE,
|
||||
@Desc("Only the outer shell is placed, leaving the interior hollow.")
|
||||
HOLLOW,
|
||||
@Desc("Quadratic radial gradient: dense at the center, thinning sharply toward the edge.")
|
||||
GRADIENT,
|
||||
@Desc("Coarse low-frequency noise that forms clumps and gaps of foliage.")
|
||||
CLUMPED,
|
||||
@Desc("Trimmed with heavy random erosion of the outer ring for a ragged, weathered look.")
|
||||
TATTERED,
|
||||
@Desc("Very thin scattering of leaves across the volume.")
|
||||
SPARSE
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.IrisProceduralBlocks;
|
||||
import com.volmit.iris.engine.object.procedural.ObjectBuilder;
|
||||
import org.bukkit.block.data.BlockData;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public final class TreePlausibility {
|
||||
private static final int MAX_DISTANCE = 7;
|
||||
private static final int[][] NEIGHBORS = {
|
||||
{1, 0, 0}, {-1, 0, 0}, {0, 1, 0}, {0, -1, 0}, {0, 0, 1}, {0, 0, -1}
|
||||
};
|
||||
|
||||
private TreePlausibility() {
|
||||
}
|
||||
|
||||
public static void apply(Map<TreeBlockCanvas.Vec, BlockData> resolved, Set<TreeBlockCanvas.Vec> trunkPositions,
|
||||
Set<TreeBlockCanvas.Vec> leafPositions, Tree tree, ObjectBuilder blocks) {
|
||||
Set<TreeBlockCanvas.Vec> realLeaves = new HashSet<>();
|
||||
for (TreeBlockCanvas.Vec v : leafPositions) {
|
||||
BlockData state = resolved.get(v);
|
||||
if (state != null && IrisProceduralBlocks.hasProperty(state, "persistent") && IrisProceduralBlocks.hasProperty(state, "distance")) {
|
||||
realLeaves.add(v);
|
||||
}
|
||||
}
|
||||
if (realLeaves.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<TreeBlockCanvas.Vec, Integer> distance = computeDistances(trunkPositions, realLeaves);
|
||||
|
||||
for (TreeBlockCanvas.Vec v : realLeaves) {
|
||||
BlockData leaf = resolved.get(v);
|
||||
Integer d = distance.get(v);
|
||||
if (!tree.plausible()) {
|
||||
leaf = withLeafState(blocks, leaf, "true", "1");
|
||||
} else if (d != null && d < MAX_DISTANCE) {
|
||||
leaf = withLeafState(blocks, leaf, "false", String.valueOf(Math.max(1, d)));
|
||||
} else {
|
||||
leaf = withLeafState(blocks, leaf, "true", String.valueOf(MAX_DISTANCE));
|
||||
}
|
||||
resolved.put(v, leaf);
|
||||
}
|
||||
}
|
||||
|
||||
private static BlockData withLeafState(ObjectBuilder blocks, BlockData leaf, String persistent, String distance) {
|
||||
return blocks.withProperty(blocks.withProperty(leaf, "persistent", persistent), "distance", distance);
|
||||
}
|
||||
|
||||
static Map<TreeBlockCanvas.Vec, Integer> computeDistances(Set<TreeBlockCanvas.Vec> trunkPositions, Set<TreeBlockCanvas.Vec> realLeaves) {
|
||||
Map<TreeBlockCanvas.Vec, Integer> distance = new HashMap<>();
|
||||
ArrayDeque<TreeBlockCanvas.Vec> queue = new ArrayDeque<>();
|
||||
|
||||
for (TreeBlockCanvas.Vec v : realLeaves) {
|
||||
for (int[] n : NEIGHBORS) {
|
||||
if (trunkPositions.contains(new TreeBlockCanvas.Vec(v.x() + n[0], v.y() + n[1], v.z() + n[2]))) {
|
||||
distance.put(v, 1);
|
||||
queue.add(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
TreeBlockCanvas.Vec v = queue.poll();
|
||||
int d = distance.get(v);
|
||||
if (d >= MAX_DISTANCE) {
|
||||
continue;
|
||||
}
|
||||
for (int[] n : NEIGHBORS) {
|
||||
TreeBlockCanvas.Vec nv = new TreeBlockCanvas.Vec(v.x() + n[0], v.y() + n[1], v.z() + n[2]);
|
||||
if (!realLeaves.contains(nv)) {
|
||||
continue;
|
||||
}
|
||||
Integer existing = distance.get(nv);
|
||||
if (existing == null || existing > d + 1) {
|
||||
distance.put(nv, d + 1);
|
||||
queue.add(nv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return distance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("Named canopy presets that drive default layer radii and shape, scaled to the tree height.")
|
||||
public enum TreeProfile {
|
||||
@Desc("Rounded broadleaf crown.")
|
||||
OAK,
|
||||
@Desc("Slim upright broadleaf crown.")
|
||||
BIRCH,
|
||||
@Desc("Narrow conifer cone.")
|
||||
SPRUCE,
|
||||
@Desc("Tall thin crown concentrated near the very top.")
|
||||
JUNGLE,
|
||||
@Desc("Flat shallow umbrella crown.")
|
||||
ACACIA,
|
||||
@Desc("Dense rounded broadleaf crown.")
|
||||
DARK_OAK,
|
||||
@Desc("Flat wide umbrella crown compressed into the top quarter (vanilla dark oak look).")
|
||||
DARK_OAK_FLAT,
|
||||
@Desc("Extra wide flat umbrella that overlaps into a continuous roofed canopy.")
|
||||
DARK_OAK_FLAT_WIDE,
|
||||
@Desc("Broad rounded blossom crown.")
|
||||
CHERRY,
|
||||
@Desc("Bare trunk with a small tuft of fronds only at the very top.")
|
||||
PALM,
|
||||
@Desc("Broad crown with wide drooping radii, ideal with a low startAngle to make a weeping skirt.")
|
||||
WILLOW,
|
||||
@Desc("Very narrow tall crown hugging the trunk (cypress / lombardy poplar).")
|
||||
COLUMNAR,
|
||||
@Desc("Squat, wide, low crown for shrubs and saplings.")
|
||||
BUSH,
|
||||
@Desc("Giant conifer cone for 2x2 mega spruce trunks.")
|
||||
MEGA_SPRUCE
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
|
||||
|
||||
public final class TreeProfiles {
|
||||
private TreeProfiles() {
|
||||
}
|
||||
|
||||
static double radiusScale(TreeProfile profile) {
|
||||
return switch (profile) {
|
||||
case OAK, DARK_OAK, CHERRY -> 1.0;
|
||||
case BIRCH -> 0.7;
|
||||
case SPRUCE -> 0.55;
|
||||
case JUNGLE -> 0.45;
|
||||
case ACACIA -> 0.6;
|
||||
case DARK_OAK_FLAT -> 1.25;
|
||||
case DARK_OAK_FLAT_WIDE -> 1.8;
|
||||
case PALM -> 0.5;
|
||||
case WILLOW -> 1.1;
|
||||
case COLUMNAR -> 0.35;
|
||||
case BUSH -> 1.3;
|
||||
case MEGA_SPRUCE -> 0.7;
|
||||
};
|
||||
}
|
||||
|
||||
static double[][] fractions(TreeProfile profile) {
|
||||
return switch (profile) {
|
||||
case OAK -> new double[][]{{0.55, 0.6}, {0.65, 0.9}, {0.75, 1.0}, {0.85, 0.9}, {0.92, 0.65}, {0.98, 0.35}};
|
||||
case BIRCH -> new double[][]{{0.6, 0.45}, {0.7, 0.75}, {0.8, 0.85}, {0.88, 0.75}, {0.94, 0.45}, {0.99, 0.2}};
|
||||
case SPRUCE, MEGA_SPRUCE -> new double[][]{{0.3, 1.0}, {0.42, 0.85}, {0.54, 0.7}, {0.65, 0.55}, {0.75, 0.4}, {0.84, 0.25}, {0.91, 0.15}, {0.97, 0.05}};
|
||||
case JUNGLE -> new double[][]{{0.82, 0.4}, {0.88, 0.8}, {0.93, 1.0}, {0.97, 0.7}, {1.0, 0.3}};
|
||||
case ACACIA -> new double[][]{{0.9, 0.5}, {0.95, 0.8}, {0.99, 0.4}};
|
||||
case DARK_OAK -> new double[][]{{0.5, 0.7}, {0.62, 1.0}, {0.72, 1.0}, {0.82, 0.9}, {0.91, 0.6}, {0.97, 0.3}};
|
||||
case DARK_OAK_FLAT -> new double[][]{{0.74, 0.62}, {0.83, 1.0}, {0.92, 1.0}, {1.0, 0.6}};
|
||||
case DARK_OAK_FLAT_WIDE -> new double[][]{{0.74, 0.62}, {0.83, 1.0}, {0.92, 1.0}, {1.0, 0.62}};
|
||||
case CHERRY -> new double[][]{{0.5, 0.5}, {0.62, 0.9}, {0.74, 1.0}, {0.84, 1.0}, {0.92, 0.7}, {0.98, 0.4}};
|
||||
case PALM -> new double[][]{{0.92, 0.45}, {0.97, 0.9}, {1.0, 1.0}, {1.03, 0.6}};
|
||||
case WILLOW -> new double[][]{{0.55, 0.7}, {0.68, 1.0}, {0.8, 1.0}, {0.9, 0.85}, {0.97, 0.6}};
|
||||
case COLUMNAR -> new double[][]{{0.4, 0.6}, {0.55, 0.9}, {0.7, 1.0}, {0.82, 0.9}, {0.92, 0.7}, {0.98, 0.4}};
|
||||
case BUSH -> new double[][]{{0.25, 0.7}, {0.45, 1.0}, {0.65, 1.0}, {0.82, 0.8}, {0.95, 0.5}};
|
||||
};
|
||||
}
|
||||
|
||||
static double[][] presetLayers(TreeProfile profile, int height, boolean branchDriven) {
|
||||
double scale = radiusScale(profile);
|
||||
int baseRadius = Math.max(3, (int) Math.round(height / 2.0 * scale));
|
||||
double[][] fractions = fractions(profile);
|
||||
|
||||
int crownExtra = branchDriven ? 0 : Math.max(2, height / 4);
|
||||
int yTop = (height - 1) + crownExtra;
|
||||
|
||||
double[][] layers = new double[fractions.length][2];
|
||||
for (int i = 0; i < fractions.length; i++) {
|
||||
int yOff = (int) Math.round(fractions[i][0] * yTop);
|
||||
double radius = Math.max(1.5, baseRadius * fractions[i][1]);
|
||||
layers[i][0] = yOff;
|
||||
layers[i][1] = radius;
|
||||
}
|
||||
return layers;
|
||||
}
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.util.math.RNG;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class TreeRootBuilder {
|
||||
private TreeRootBuilder() {
|
||||
}
|
||||
|
||||
public static void build(TreeBlockCanvas canvas, Tree tree, int height, long baseSeed) {
|
||||
List<int[]> cells = new ArrayList<>();
|
||||
for (TreeBlockCanvas.Vec v : canvas.getTrunk()) {
|
||||
if (v.y() == 0) {
|
||||
cells.add(new int[]{v.x(), v.z()});
|
||||
}
|
||||
}
|
||||
if (cells.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int depth = tree.rootDepth() > 0 ? tree.rootDepth() : autoDepth(height);
|
||||
RNG rng = new RNG(baseSeed ^ 0x5009L);
|
||||
|
||||
double cx = 0;
|
||||
double cz = 0;
|
||||
for (int[] c : cells) {
|
||||
cx += c[0];
|
||||
cz += c[1];
|
||||
}
|
||||
cx /= cells.size();
|
||||
cz /= cells.size();
|
||||
|
||||
double baseRadius = 1.0;
|
||||
for (int[] c : cells) {
|
||||
baseRadius = Math.max(baseRadius, Math.hypot(c[0] - cx, c[1] - cz) + 0.5);
|
||||
}
|
||||
double flare = tree.rootFlare() > 0 ? tree.rootFlare() : baseRadius + Math.max(2.0, depth * 0.6);
|
||||
|
||||
TreeRootStyle style = tree.rootStyle();
|
||||
switch (style) {
|
||||
case TAPROOT -> taproot(canvas, cells, cx, cz, baseRadius, depth);
|
||||
case BUTTRESS -> {
|
||||
taproot(canvas, cells, cx, cz, baseRadius, depth);
|
||||
buttress(canvas, cells, cx, cz, baseRadius, depth, flare, rng);
|
||||
}
|
||||
case STILT -> {
|
||||
taproot(canvas, cells, cx, cz, baseRadius, Math.min(depth, 3));
|
||||
stilts(canvas, cx, cz, baseRadius, flare, height, cells.size(), rng);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void taproot(TreeBlockCanvas canvas, List<int[]> cells, double cx, double cz, double baseRadius, int depth) {
|
||||
int icx = (int) Math.round(cx);
|
||||
int icz = (int) Math.round(cz);
|
||||
for (int k = 1; k <= depth; k++) {
|
||||
double frac = k / (double) depth;
|
||||
double keepR = baseRadius * (1.0 - 0.6 * frac);
|
||||
for (int[] c : cells) {
|
||||
if (Math.hypot(c[0] - cx, c[1] - cz) <= keepR + 1e-6) {
|
||||
placeRoot(canvas, c[0], -k, c[1], TreeBlockCanvas.Axis.Y);
|
||||
}
|
||||
}
|
||||
placeRoot(canvas, icx, -k, icz, TreeBlockCanvas.Axis.Y);
|
||||
}
|
||||
}
|
||||
|
||||
private static void buttress(TreeBlockCanvas canvas, List<int[]> cells, double cx, double cz, double baseRadius,
|
||||
int depth, double flare, RNG rng) {
|
||||
int nLegs = Math.max(4, Math.min(12, cells.size() + 2));
|
||||
for (int li = 0; li < nLegs; li++) {
|
||||
double ang = 2.0 * Math.PI * li / nLegs + rng.d(-0.25, 0.25);
|
||||
double ca = Math.cos(ang);
|
||||
double sa = Math.sin(ang);
|
||||
int prevX = (int) Math.round(cx + baseRadius * ca);
|
||||
int prevZ = (int) Math.round(cz + baseRadius * sa);
|
||||
for (int k = 1; k <= depth; k++) {
|
||||
double frac = k / (double) depth;
|
||||
double r = baseRadius + (flare - baseRadius) * frac;
|
||||
int x = (int) Math.round(cx + r * ca);
|
||||
int z = (int) Math.round(cz + r * sa);
|
||||
placeRoot(canvas, x, -k, z, TreeBlockCanvas.Axis.Y);
|
||||
if (x != prevX || z != prevZ) {
|
||||
placeRoot(canvas, prevX, -k, prevZ, TreeBlockCanvas.Axis.Y);
|
||||
}
|
||||
prevX = x;
|
||||
prevZ = z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void stilts(TreeBlockCanvas canvas, double cx, double cz, double baseRadius, double flare,
|
||||
int height, int cellCount, RNG rng) {
|
||||
int stiltTop = Math.max(2, Math.min(7, (int) Math.round(0.25 * height)));
|
||||
int nProps = Math.max(4, Math.min(10, cellCount + 2));
|
||||
for (int p = 0; p < nProps; p++) {
|
||||
double ang = 2.0 * Math.PI * p / nProps + rng.d(-0.25, 0.25);
|
||||
double ca = Math.cos(ang);
|
||||
double sa = Math.sin(ang);
|
||||
int sx = (int) Math.round(cx + baseRadius * ca);
|
||||
int sz = (int) Math.round(cz + baseRadius * sa);
|
||||
int fx = (int) Math.round(cx + flare * ca);
|
||||
int fz = (int) Math.round(cz + flare * sa);
|
||||
rasterizeRoot(canvas, sx, stiltTop, sz, fx, -2, fz);
|
||||
}
|
||||
}
|
||||
|
||||
private static void rasterizeRoot(TreeBlockCanvas canvas, int ox, int oy, int oz, int ex, int ey, int ez) {
|
||||
int dx = ex - ox;
|
||||
int dy = ey - oy;
|
||||
int dz = ez - oz;
|
||||
int steps = Math.max(Math.max(Math.abs(dx), Math.abs(dy)), Math.max(Math.abs(dz), 1));
|
||||
TreeBlockCanvas.Axis axis = TreeFunctions.logAxis(dx, dy, dz);
|
||||
for (int i = 0; i <= steps; i++) {
|
||||
double t = i / (double) steps;
|
||||
int x = (int) Math.round(ox + dx * t);
|
||||
int y = (int) Math.round(oy + dy * t);
|
||||
int z = (int) Math.round(oz + dz * t);
|
||||
placeRoot(canvas, x, y, z, axis);
|
||||
}
|
||||
}
|
||||
|
||||
private static void placeRoot(TreeBlockCanvas canvas, int x, int y, int z, TreeBlockCanvas.Axis axis) {
|
||||
if (!canvas.has(x, y, z)) {
|
||||
canvas.setTrunk(x, y, z, TreeBlockCanvas.Role.TRUNK, axis);
|
||||
}
|
||||
}
|
||||
|
||||
private static int autoDepth(int height) {
|
||||
return Math.max(2, Math.min(16, (int) Math.round(0.18 * height)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
|
||||
@Desc("How a procedural tree's root system is built below (and around) the trunk base.")
|
||||
public enum TreeRootStyle {
|
||||
@Desc("A single tapering taproot straight down from the base.")
|
||||
TAPROOT,
|
||||
@Desc("A taproot plus a ring of flared buttress legs that splay outward as they descend (default).")
|
||||
BUTTRESS,
|
||||
@Desc("Visible above-ground prop roots that arch out and down from the lower trunk to the ground (mangrove style).")
|
||||
STILT
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
import com.volmit.iris.engine.object.annotations.Snippet;
|
||||
import com.volmit.iris.engine.object.annotations.MinNumber;
|
||||
import com.volmit.iris.engine.object.annotations.RegistryListBlockType;
|
||||
import com.volmit.iris.engine.object.annotations.Required;
|
||||
|
||||
@Snippet("tree-secondary-leaf")
|
||||
@Desc("A weighted accent leaf or foliage block scattered through the canopy (blossoms, shroomlight, berries).")
|
||||
public final class TreeSecondaryLeaf {
|
||||
@Required
|
||||
@Desc("The block id to scatter, e.g. minecraft:shroomlight")
|
||||
@RegistryListBlockType
|
||||
private String block = "";
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("The relative weight of this block among the secondary leaves.")
|
||||
private int weight = 1;
|
||||
|
||||
public TreeSecondaryLeaf() {
|
||||
}
|
||||
|
||||
public String block() {
|
||||
return block;
|
||||
}
|
||||
|
||||
public int weight() {
|
||||
return weight;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.annotations.Desc;
|
||||
import com.volmit.iris.engine.object.annotations.Snippet;
|
||||
import com.volmit.iris.engine.object.annotations.MaxNumber;
|
||||
import com.volmit.iris.engine.object.annotations.MinNumber;
|
||||
|
||||
@Snippet("tree-sub-branches")
|
||||
@Desc("One recursive level of sub-branches sprouting from each primary branch tip.")
|
||||
public final class TreeSubBranches {
|
||||
@MinNumber(1)
|
||||
@Desc("How many sub-branches sprout from each primary branch tip.")
|
||||
private int count = 1;
|
||||
|
||||
@Desc("Pitch deflection in degrees applied to each sub-branch relative to its parent. Positive bends up, negative droops down. Cumulative pitch past 90 makes sub-branches point downward.")
|
||||
private double pitchDelta = 0;
|
||||
|
||||
@Desc("Yaw (horizontal) spread in degrees fanned between sub-branches.")
|
||||
private double yawDelta = 45;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Sub-branch length as a fraction of its parent branch length.")
|
||||
private double lengthScale = 0.5;
|
||||
|
||||
@MinNumber(0)
|
||||
@MaxNumber(1)
|
||||
@Desc("How much each sub-branch sags/droops along its length (catenary).")
|
||||
private double sag = 0;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Leaf ball radius placed at each sub-branch tip.")
|
||||
private int clusterRadius = 1;
|
||||
|
||||
@Desc("How the sub-branch tip leaf cluster fills.")
|
||||
private TreeLeafMode clusterMode = TreeLeafMode.TRIMMED;
|
||||
|
||||
@MinNumber(0)
|
||||
@Desc("Fill density for the sub-branch leaf cluster when clusterMode is density or noise.")
|
||||
private double clusterDensity = 0.85;
|
||||
|
||||
public TreeSubBranches() {
|
||||
}
|
||||
|
||||
public int count() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public double pitchDelta() {
|
||||
return pitchDelta;
|
||||
}
|
||||
|
||||
public double yawDelta() {
|
||||
return yawDelta;
|
||||
}
|
||||
|
||||
public double lengthScale() {
|
||||
return lengthScale;
|
||||
}
|
||||
|
||||
public double sag() {
|
||||
return sag;
|
||||
}
|
||||
|
||||
public int clusterRadius() {
|
||||
return clusterRadius;
|
||||
}
|
||||
|
||||
public TreeLeafMode clusterMode() {
|
||||
return clusterMode == null ? TreeLeafMode.TRIMMED : clusterMode;
|
||||
}
|
||||
|
||||
public double clusterDensity() {
|
||||
return clusterDensity;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
|
||||
public final class TreeSupport {
|
||||
private static final int LEGAL_DISTANCE = 6;
|
||||
private static final int REACH_GAP = 4;
|
||||
|
||||
private TreeSupport() {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private static void 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();
|
||||
int length = Math.max(Math.max(Math.abs(dx), Math.abs(dy)), Math.max(Math.abs(dz), 1));
|
||||
int steps = Math.max(1, length - REACH_GAP);
|
||||
TreeBlockCanvas.Axis axis = TreeFunctions.logAxis(dx, dy, dz);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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()));
|
||||
}
|
||||
}
|
||||
+200
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* Iris is a World Generator for Minecraft Bukkit Servers
|
||||
* Copyright (c) 2022 Arcane Arts (Volmit Software)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.volmit.iris.engine.object.procedural.tree;
|
||||
|
||||
import com.volmit.iris.engine.object.procedural.IrisProceduralBlocks;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class TreeTrunkBuilder {
|
||||
public record Limb(double[][] offsets, int branchStartY) {
|
||||
}
|
||||
|
||||
private TreeTrunkBuilder() {
|
||||
}
|
||||
|
||||
public static List<Limb> build(TreeBlockCanvas canvas, Tree tree, int height) {
|
||||
boolean hasSecondary = hasSecondaryTrunk(tree);
|
||||
double[][] mainOffsets = leanPath(tree, height);
|
||||
int forks = Math.max(1, tree.trunkForks());
|
||||
|
||||
List<Limb> limbs = new ArrayList<>();
|
||||
if (forks == 1) {
|
||||
placeColumn(canvas, tree, mainOffsets, height, 0, height - 1, hasSecondary);
|
||||
limbs.add(new Limb(mainOffsets, 0));
|
||||
markExposedEnds(canvas);
|
||||
return limbs;
|
||||
}
|
||||
|
||||
int forkY = Math.max(1, Math.min(height - 2, (int) Math.round(tree.forkHeight() * (height - 1))));
|
||||
placeColumn(canvas, tree, mainOffsets, height, 0, forkY, hasSecondary);
|
||||
|
||||
double baseCx = mainOffsets[forkY][0];
|
||||
double baseCz = mainOffsets[forkY][1];
|
||||
double reachMax = (height - forkY) * Math.tan(Math.toRadians(tree.forkAngle()));
|
||||
|
||||
for (int f = 0; f < forks; f++) {
|
||||
double az = Math.toRadians((360.0 / forks) * f);
|
||||
double[][] forkOffsets = new double[height][2];
|
||||
for (int y = 0; y <= forkY; y++) {
|
||||
forkOffsets[y][0] = mainOffsets[y][0];
|
||||
forkOffsets[y][1] = mainOffsets[y][1];
|
||||
}
|
||||
for (int y = forkY + 1; y < height; y++) {
|
||||
double p = (y - forkY) / (double) Math.max(1, height - 1 - forkY);
|
||||
double reach = reachMax * p;
|
||||
forkOffsets[y][0] = baseCx + reach * Math.sin(az);
|
||||
forkOffsets[y][1] = baseCz + reach * Math.cos(az);
|
||||
}
|
||||
placeColumn(canvas, tree, forkOffsets, height, forkY + 1, height - 1, hasSecondary);
|
||||
limbs.add(new Limb(forkOffsets, forkY));
|
||||
}
|
||||
|
||||
markExposedEnds(canvas);
|
||||
return limbs;
|
||||
}
|
||||
|
||||
private static double[][] leanPath(Tree tree, int height) {
|
||||
double[][] offsets = new double[height][2];
|
||||
for (int y = 0; y < height; y++) {
|
||||
double[] lean = leanOffset(tree, y, height);
|
||||
offsets[y][0] = lean[0];
|
||||
offsets[y][1] = lean[1];
|
||||
}
|
||||
return offsets;
|
||||
}
|
||||
|
||||
private static void placeColumn(TreeBlockCanvas canvas, Tree tree, double[][] offsets, int height,
|
||||
int yStart, int yEnd, boolean hasSecondary) {
|
||||
double prevCx = yStart > 0 ? offsets[yStart - 1][0] : 0.0;
|
||||
double prevCz = yStart > 0 ? offsets[yStart - 1][1] : 0.0;
|
||||
|
||||
for (int y = yStart; y <= yEnd; y++) {
|
||||
double cx = offsets[y][0];
|
||||
double cz = offsets[y][1];
|
||||
int w = widthAt(tree, y, height);
|
||||
TreeBlockCanvas.Axis axis = TreeFunctions.logAxis(cx - prevCx, 1.0, cz - prevCz);
|
||||
TreeBlockCanvas.Role role = trunkRole(tree, hasSecondary, y, height);
|
||||
|
||||
for (int[] xz : squarePositions(cx, cz, w)) {
|
||||
canvas.setTrunk(xz[0], y, xz[1], role, axis);
|
||||
}
|
||||
|
||||
double shift = Math.hypot(cx - prevCx, cz - prevCz);
|
||||
if (shift > 1.0) {
|
||||
int steps = (int) Math.ceil(shift);
|
||||
for (int s = 1; s < steps; s++) {
|
||||
double tFill = s / (double) steps;
|
||||
double icx = prevCx + (cx - prevCx) * tFill;
|
||||
double icz = prevCz + (cz - prevCz) * tFill;
|
||||
int fillY = (int) Math.round(y - 1 + tFill);
|
||||
for (int[] xz : squarePositions(icx, icz, w)) {
|
||||
if (!canvas.has(xz[0], fillY, xz[1])) {
|
||||
canvas.setTrunk(xz[0], fillY, xz[1], role, axis);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prevCx = cx;
|
||||
prevCz = cz;
|
||||
}
|
||||
}
|
||||
|
||||
private static void markExposedEnds(TreeBlockCanvas canvas) {
|
||||
for (TreeBlockCanvas.Vec v : new ArrayList<>(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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int widthAt(Tree tree, int y, int height) {
|
||||
double t = y / (double) Math.max(height - 1, 1);
|
||||
double multiplier = TreeFunctions.trunkWidthMultiplier(tree, t);
|
||||
return Math.max(1, (int) Math.round(tree.trunkWidth() * multiplier));
|
||||
}
|
||||
|
||||
private static TreeBlockCanvas.Role trunkRole(Tree tree, boolean hasSecondary, int y, int height) {
|
||||
if (!hasSecondary) {
|
||||
return TreeBlockCanvas.Role.TRUNK;
|
||||
}
|
||||
double t = y / (double) Math.max(height - 1, 1);
|
||||
if (tree.secondaryTrunkStart() <= t && t <= tree.secondaryTrunkEnd()) {
|
||||
return TreeBlockCanvas.Role.SECONDARY_TRUNK;
|
||||
}
|
||||
return TreeBlockCanvas.Role.TRUNK;
|
||||
}
|
||||
|
||||
private static boolean hasSecondaryTrunk(Tree tree) {
|
||||
if (IrisProceduralBlocks.paletteSet(tree.secondaryTrunkPalette())) {
|
||||
return true;
|
||||
}
|
||||
return tree.secondaryTrunk().filter(trunk -> !trunk.isEmpty()).isPresent();
|
||||
}
|
||||
|
||||
private static double[] leanOffset(Tree tree, int y, int height) {
|
||||
if (tree.leanAngle() == 0.0 || height <= 1) {
|
||||
return new double[]{0.0, 0.0};
|
||||
}
|
||||
double t = y / (double) Math.max(height - 1, 1);
|
||||
double progress = TreeFunctions.curveProgress(tree.trunkCurve(), t, tree.curveSteepness());
|
||||
double maxReach = height * Math.tan(Math.toRadians(tree.leanAngle()));
|
||||
double reach = maxReach * progress;
|
||||
double azDeg = tree.leanAzimuthMode() == TreeAzimuthMode.CONSTANT
|
||||
? tree.leanAzimuth()
|
||||
: TreeFunctions.azimuthDegrees(tree.leanAzimuthMode(), t, -1, tree, tree.leanAzimuth(), null);
|
||||
double azRad = Math.toRadians(azDeg);
|
||||
double dx = reach * Math.sin(azRad);
|
||||
double dz = reach * Math.cos(azRad);
|
||||
return new double[]{dx, dz};
|
||||
}
|
||||
|
||||
private static List<int[]> squarePositions(double cx, double cz, int width) {
|
||||
List<int[]> out = new ArrayList<>();
|
||||
if (width % 2 == 1) {
|
||||
int half = width / 2;
|
||||
int icx = (int) Math.round(cx);
|
||||
int icz = (int) Math.round(cz);
|
||||
for (int dx = -half; dx <= half; dx++) {
|
||||
for (int dz = -half; dz <= half; dz++) {
|
||||
out.add(new int[]{icx + dx, icz + dz});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
int ox = (int) Math.floor(cx) - width / 2 + 1;
|
||||
int oz = (int) Math.floor(cz) - width / 2 + 1;
|
||||
for (int dx = 0; dx < width; dx++) {
|
||||
for (int dz = 0; dz < width; dz++) {
|
||||
out.add(new int[]{ox + dx, oz + dz});
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static String stripState(String block) {
|
||||
if (block == null) {
|
||||
return null;
|
||||
}
|
||||
int idx = block.indexOf('[');
|
||||
return idx < 0 ? block : block.substring(0, idx);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user