diff --git a/core/src/main/java/com/volmit/iris/engine/HybridBiomeSelector.java b/core/src/main/java/com/volmit/iris/engine/HybridBiomeSelector.java
new file mode 100644
index 000000000..041169940
--- /dev/null
+++ b/core/src/main/java/com/volmit/iris/engine/HybridBiomeSelector.java
@@ -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 .
+ */
+
+package com.volmit.iris.engine;
+
+import com.volmit.iris.core.loader.IrisData;
+import com.volmit.iris.engine.object.IrisBiome;
+import com.volmit.iris.util.math.RNG;
+import com.volmit.iris.util.noise.CNG;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Chooses biomes for HYBRID terrain. Biomes whose placement allows the base terrain height
+ * compete through their own noise, so a biome dropping out never reshuffles the others.
+ */
+public class HybridBiomeSelector {
+ private final RNG rng;
+ private final IrisData data;
+ private final Map competitionNoiseByBiome = new ConcurrentHashMap<>();
+
+ public HybridBiomeSelector(RNG rng, IrisData data) {
+ this.rng = rng;
+ this.data = data;
+ }
+
+ public IrisBiome select(List candidates, double placementHeight, double x, double z) {
+ IrisBiome strongest = strongestAllowedBiome(candidates, placementHeight, x, z);
+ return strongest != null ? strongest : closestBiomeByHeight(candidates, placementHeight);
+ }
+
+ private IrisBiome strongestAllowedBiome(List candidates, double placementHeight, double x, double z) {
+ IrisBiome strongest = null;
+ double strongestScore = Double.NEGATIVE_INFINITY;
+
+ for (IrisBiome biome : candidates) {
+ if (!biome.getPlacement().allowsHeight(placementHeight)) {
+ continue;
+ }
+
+ double score = competitionScore(biome, x, z);
+
+ if (score > strongestScore) {
+ strongest = biome;
+ strongestScore = score;
+ }
+ }
+
+ return strongest;
+ }
+
+ private double competitionScore(IrisBiome biome, double x, double z) {
+ return competitionNoise(biome).noise(x, z) / biome.getRarity();
+ }
+
+ private CNG competitionNoise(IrisBiome biome) {
+ return competitionNoiseByBiome.computeIfAbsent(biome.getLoadKey(), key -> biome.getPlacement()
+ .getCompetitionStyle()
+ .createNoCache(rng.nextParallelRNG(key.hashCode()), data));
+ }
+
+ private IrisBiome closestBiomeByHeight(List candidates, double placementHeight) {
+ IrisBiome closest = null;
+ double closestDistance = Double.POSITIVE_INFINITY;
+
+ for (IrisBiome biome : candidates) {
+ double distance = biome.getPlacement().distanceToHeightRange(placementHeight);
+
+ if (distance < closestDistance) {
+ closest = biome;
+ closestDistance = distance;
+ }
+ }
+
+ return closest;
+ }
+}
diff --git a/core/src/main/java/com/volmit/iris/engine/IrisComplex.java b/core/src/main/java/com/volmit/iris/engine/IrisComplex.java
index 98e73444e..90f2e63f2 100644
--- a/core/src/main/java/com/volmit/iris/engine/IrisComplex.java
+++ b/core/src/main/java/com/volmit/iris/engine/IrisComplex.java
@@ -48,6 +48,7 @@ import java.util.*;
@ToString(exclude = "data")
public class IrisComplex implements DataProvider {
private static final BlockData AIR = Material.AIR.createBlockData();
+ private static final List HYBRID_PLACED_TYPES = List.of(InferredType.LAND, InferredType.SEA, InferredType.SHORE);
private RNG rng;
private double fluidHeight;
private IrisData data;
@@ -69,6 +70,8 @@ public class IrisComplex implements DataProvider {
private ProceduralStream roundedHeighteightStream;
private ProceduralStream maxHeightStream;
private ProceduralStream overlayStream;
+ private ProceduralStream placementHeightStream;
+ private HybridBiomeSelector hybridBiomeSelector;
private ProceduralStream heightFluidStream;
private ProceduralStream slopeStream;
private ProceduralStream topSurfaceStream;
@@ -132,6 +135,10 @@ public class IrisComplex implements DataProvider {
: regionStyleStream
.selectRarity(data.getRegionLoader().loadAll(engine.getDimension().getRegions()), legacy)
.cache2D("regionStream", engine, cacheSize).waste("Region Stream");
+ boolean hybridTerrain = engine.getDimension().getTerrainMode() == IrisTerrainMode.HYBRID;
+ if (hybridTerrain) {
+ prepareHybridBiomePlacement(engine, cacheSize);
+ }
regionIDStream = regionIdentityStream.convertCached((i) -> new UUID(Double.doubleToLongBits(i),
String.valueOf(i * 38445).hashCode() * 3245556666L)).waste("Region ID Stream");
caveBiomeStream = regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
@@ -143,7 +150,7 @@ public class IrisComplex implements DataProvider {
.onNull(emptyBiome)
).convertAware2D(ProceduralStream::get).cache2D("caveBiomeStream", engine, cacheSize).waste("Cave Biome Stream");
inferredStreams.put(InferredType.CAVE, caveBiomeStream);
- landBiomeStream = regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
+ landBiomeStream = hybridTerrain ? createHybridBiomeStream(engine, InferredType.LAND, cacheSize) : regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
.convert((r)
-> engine.getDimension().getLandBiomeStyle().create(rng.nextParallelRNG(InferredType.LAND.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
@@ -153,7 +160,7 @@ public class IrisComplex implements DataProvider {
).convertAware2D(ProceduralStream::get)
.cache2D("landBiomeStream", engine, cacheSize).waste("Land Biome Stream");
inferredStreams.put(InferredType.LAND, landBiomeStream);
- seaBiomeStream = regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
+ seaBiomeStream = hybridTerrain ? createHybridBiomeStream(engine, InferredType.SEA, cacheSize) : regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
.convert((r)
-> engine.getDimension().getSeaBiomeStyle().create(rng.nextParallelRNG(InferredType.SEA.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
@@ -163,7 +170,7 @@ public class IrisComplex implements DataProvider {
).convertAware2D(ProceduralStream::get)
.cache2D("seaBiomeStream", engine, cacheSize).waste("Sea Biome Stream");
inferredStreams.put(InferredType.SEA, seaBiomeStream);
- shoreBiomeStream = regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
+ shoreBiomeStream = hybridTerrain ? createHybridBiomeStream(engine, InferredType.SHORE, cacheSize) : regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
.convert((r)
-> engine.getDimension().getShoreBiomeStyle().create(rng.nextParallelRNG(InferredType.SHORE.ordinal()), getData()).stream()
.zoom(engine.getDimension().getBiomeZoom())
@@ -173,6 +180,7 @@ public class IrisComplex implements DataProvider {
inferredStreams.put(InferredType.SHORE, shoreBiomeStream);
bridgeStream = focusBiome != null ? ProceduralStream.of((x, z) -> focusBiome.getInferredType(),
Interpolated.of(a -> 0D, a -> focusBiome.getInferredType())) :
+ hybridTerrain ? overlayStream.convert(this::inferTypeFromOverlay).cache2D("bridgeStream", engine, cacheSize).waste("Bridge Stream") :
engine.getDimension().getContinentalStyle().create(rng.nextParallelRNG(234234565), getData())
.bake().scale(1D / engine.getDimension().getContinentZoom()).bake().stream()
.convert((v) -> v >= engine.getDimension().getLandChance() ? InferredType.SEA : InferredType.LAND)
@@ -243,6 +251,53 @@ public class IrisComplex implements DataProvider {
return null;
}
+ private void prepareHybridBiomePlacement(Engine engine, int cacheSize) {
+ IrisShapedGeneratorStyle jitter = engine.getDimension().getBiomePlacementJitter();
+ RNG jitterRng = rng.nextParallelRNG(5_118_772);
+ placementHeightStream = overlayStream.add((x, z) -> jitter.get(jitterRng, getData(), x, z))
+ .cache2D("placementHeightStream", engine, cacheSize).waste("Placement Height Stream");
+ hybridBiomeSelector = new HybridBiomeSelector(rng.nextParallelRNG(9_302_115), data);
+ placedRegions(engine).forEach(this::assignInferredTypes);
+ }
+
+ private List placedRegions(Engine engine) {
+ if (focusRegion != null) {
+ return List.of(focusRegion);
+ }
+
+ return data.getRegionLoader().loadAll(engine.getDimension().getRegions());
+ }
+
+ private void assignInferredTypes(IrisRegion region) {
+ for (InferredType type : HYBRID_PLACED_TYPES) {
+ region.getBiomes(this, type).forEach(biome -> biome.setInferredType(type));
+ }
+ }
+
+ private ProceduralStream createHybridBiomeStream(Engine engine, InferredType type, int cacheSize) {
+ String name = type.name().toLowerCase() + "BiomeStream";
+ return regionStream.contextInjecting((c, x, z) -> IrisContext.getOr(engine).getChunkContext().getRegion().get(x, z))
+ .convertAware2D((region, x, z) -> {
+ double zoom = hybridBiomeZoom(engine.getDimension(), region, type);
+ return hybridBiomeSelector.select(region.getBiomes(this, type), placementHeightStream.get(x, z), x / zoom, z / zoom);
+ })
+ .cache2D(name, engine, cacheSize).waste(name);
+ }
+
+ private static double hybridBiomeZoom(IrisDimension dimension, IrisRegion region, InferredType type) {
+ double typeZoom = switch (type) {
+ case LAND -> dimension.getLandZoom();
+ case SEA -> dimension.getSeaZoom();
+ default -> 1;
+ };
+
+ return dimension.getBiomeZoom() * typeZoom * region.getBiomeZoom(type);
+ }
+
+ private InferredType inferTypeFromOverlay(double heightAboveFluid) {
+ return heightAboveFluid < 0 ? InferredType.SEA : InferredType.LAND;
+ }
+
private IrisRegion findRegion(IrisBiome focus, Engine engine) {
for (IrisRegion i : engine.getDimension().getAllRegions(engine)) {
if (i.getAllBiomeIds().contains(focus.getLoadKey())) {
diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisBiome.java b/core/src/main/java/com/volmit/iris/engine/object/IrisBiome.java
index 80cd6f771..85f4b4566 100644
--- a/core/src/main/java/com/volmit/iris/engine/object/IrisBiome.java
+++ b/core/src/main/java/com/volmit/iris/engine/object/IrisBiome.java
@@ -163,6 +163,8 @@ public class IrisBiome extends IrisRegistrant implements IRare {
@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.")
private KList generators = new KList().qadd(new IrisBiomeGeneratorLink());
+ @Desc("Where this biome may be chosen when the dimension uses HYBRID terrain")
+ private IrisBiomePlacement placement = new IrisBiomePlacement();
@ArrayType(min = 1, type = IrisDepositGenerator.class)
@Desc("Define biome deposit generators that add onto the existing regional and global deposit generators")
private KList deposits = new KList<>();
diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisBiomePlacement.java b/core/src/main/java/com/volmit/iris/engine/object/IrisBiomePlacement.java
new file mode 100644
index 000000000..254e2ff36
--- /dev/null
+++ b/core/src/main/java/com/volmit/iris/engine/object/IrisBiomePlacement.java
@@ -0,0 +1,59 @@
+/*
+ * 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 .
+ */
+
+package com.volmit.iris.engine.object;
+
+import com.volmit.iris.engine.object.annotations.Desc;
+import com.volmit.iris.engine.object.annotations.MaxNumber;
+import com.volmit.iris.engine.object.annotations.MinNumber;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+@Accessors(chain = true)
+@NoArgsConstructor
+@AllArgsConstructor
+@Desc("Where this biome may be chosen when the dimension uses HYBRID terrain. Ignored by BIOME terrain.")
+@Data
+public class IrisBiomePlacement {
+ @MinNumber(-2032) // TODO: WARNING HEIGHT
+ @MaxNumber(2032) // TODO: WARNING HEIGHT
+ @Desc("The lowest base terrain height this biome is chosen on, relative to the fluid height. Use negative for oceans.")
+ private int minHeight = -2032;
+
+ @MinNumber(-2032) // TODO: WARNING HEIGHT
+ @MaxNumber(2032) // TODO: WARNING HEIGHT
+ @Desc("The highest base terrain height this biome is chosen on, relative to the fluid height.")
+ private int maxHeight = 2032;
+
+ @Desc("Where several biomes may be chosen, each samples this noise and the highest value wins. The biome's rarity divides its value. Zoomed by the dimension and region biome zooms.")
+ private IrisGeneratorStyle competitionStyle = NoiseStyle.SIMPLEX.style();
+
+ public boolean allowsHeight(double height) {
+ return height >= minHeight && height <= maxHeight;
+ }
+
+ public double distanceToHeightRange(double height) {
+ if (height < minHeight) {
+ return minHeight - height;
+ }
+
+ return Math.max(0, height - maxHeight);
+ }
+}
diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisDimension.java b/core/src/main/java/com/volmit/iris/engine/object/IrisDimension.java
index 53f6a23dc..96f7707f3 100644
--- a/core/src/main/java/com/volmit/iris/engine/object/IrisDimension.java
+++ b/core/src/main/java/com/volmit/iris/engine/object/IrisDimension.java
@@ -219,6 +219,10 @@ public class IrisDimension extends IrisRegistrant {
@ArrayType(min = 1, type = IrisShapedGeneratorStyle.class)
@Desc("Overlay additional noise on top of the interoplated terrain.")
private KList overlayNoise = new KList<>();
+ @Desc("Where the shape of the terrain comes from. HYBRID builds the base terrain from the overlay noise and chooses biomes by its height.")
+ private IrisTerrainMode terrainMode = IrisTerrainMode.BIOME;
+ @Desc("Only used by HYBRID terrain. Shifts the height biomes are chosen by, so biome borders don't follow the terrain's contour lines.")
+ private IrisShapedGeneratorStyle biomePlacementJitter = new IrisShapedGeneratorStyle(NoiseStyle.SIMPLEX, -8, 8);
@Desc("If true, the spawner system has infinite energy. This is NOT recommended because it would allow for mobs to keep spawning over and over without a rate limit")
private boolean infiniteEnergy = false;
@MinNumber(0)
diff --git a/core/src/main/java/com/volmit/iris/engine/object/IrisTerrainMode.java b/core/src/main/java/com/volmit/iris/engine/object/IrisTerrainMode.java
new file mode 100644
index 000000000..8fcfeac10
--- /dev/null
+++ b/core/src/main/java/com/volmit/iris/engine/object/IrisTerrainMode.java
@@ -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 .
+ */
+
+package com.volmit.iris.engine.object;
+
+import com.volmit.iris.engine.object.annotations.Desc;
+
+@Desc("Where the shape of the terrain comes from")
+public enum IrisTerrainMode {
+ @Desc("Biomes are placed by noise without looking at height, and each biome shapes its own terrain with its generators. The overlay noise is added on top.")
+ BIOME,
+
+ @Desc("The overlay noise shapes the base terrain. Land, sea and biomes are chosen by the height of that base terrain (see each biome's placement), then each biome adds its generators on top.")
+ HYBRID
+}