Add hybrid terrain mode

Overlay shapes land; biomes pick by height.
This commit is contained in:
piexel committed 2026-10-21 14:51:32 +02:00
1 parent 9eb56543a3
commit ea42141550
6 files changed
+247 -3

No files matched your search

@@ -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;
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<String, CNG> competitionNoiseByBiome = new ConcurrentHashMap<>();
public HybridBiomeSelector(RNG rng, IrisData data) {
this.rng = rng;
this.data = data;
}
public IrisBiome select(List<IrisBiome> 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<IrisBiome> 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<IrisBiome> 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;
}
}
@@ -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<InferredType> 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<Integer> roundedHeighteightStream;
private ProceduralStream<Double> maxHeightStream;
private ProceduralStream<Double> overlayStream;
private ProceduralStream<Double> placementHeightStream;
private HybridBiomeSelector hybridBiomeSelector;
private ProceduralStream<Double> heightFluidStream;
private ProceduralStream<Double> slopeStream;
private ProceduralStream<Integer> 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<IrisRegion> 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<IrisBiome> 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())) {
@@ -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<IrisBiomeGeneratorLink> generators = new KList<IrisBiomeGeneratorLink>().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<IrisDepositGenerator> deposits = new KList<>();
@@ -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 <https://www.gnu.org/licenses/>.
*/
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);
}
}
@@ -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<IrisShapedGeneratorStyle> 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)
@@ -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;
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
}