Hydrology climate zones

- Hydrology climate splits the land into wet and dry zones with a noise style
- Dry zones keep only the dryRainfall share of rainfall and pond density
- Zone edges ease across the transition band
- Policy climateZones exempts an area from the dry zones
- Policy fluid also fills streams and rivers that flow through the area
- Bank steepness reads the steepest natural ground between the bank and the water
- Bank edges roughen with smooth noise
- Natural ground heights reach the rasterizer at full precision
- Settings reject climate values outside 0..1
- Hydrology probes read the full precision natural heights
This commit is contained in:
Brian Neumann-Fopiano committed 2026-10-31 19:23:25 -04:00
1 parent 304e66b74b
commit c2eca3e6bd
26 files changed
+427 -73

No files matched your search

@@ -86,7 +86,7 @@ final class HydrologyBlockMetrics {
@Override
public int natural(int x, int z) {
return inputs.naturalHeight().applyAsInt(x, z);
return (int) Math.round(inputs.naturalHeight().height(x, z));
}
};
int minX = key.wx() * WindowKey.SIZE;
@@ -11,10 +11,12 @@ import art.arcane.iris.generation.hydrology.plan.RegionPlan;
import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.hydrology.plan.WindowPlan;
import art.arcane.iris.generation.hydrology.plan.WindowPlanner;
import art.arcane.iris.generation.hydrology.raster.ChannelFluids;
import art.arcane.iris.generation.hydrology.raster.ChunkRasterizer;
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
import art.arcane.iris.generation.hydrology.raster.ReachIndex;
import art.arcane.iris.generation.hydrology.water.Pond;
import art.arcane.iris.generation.hydrology.raster.NaturalHeights;
import com.sun.management.OperatingSystemMXBean;
import java.lang.management.ManagementFactory;
@@ -32,7 +34,6 @@ import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction;
import java.util.function.IntBinaryOperator;
final class HydrologyPlanHarness {
private static final double TILE_AREA = 1024D * 1024D;
@@ -43,7 +44,7 @@ final class HydrologyPlanHarness {
static final class Plans {
private final HydrologySettings settings;
private final IntBinaryOperator naturalHeight;
private final NaturalHeights naturalHeight;
private final HydrologyWindows.WindowRange planned;
private final CountingTerrain counting;
private final HydrologyPlanners planners;
@@ -54,13 +55,13 @@ final class HydrologyPlanHarness {
private final Map<WindowKey, Double> windowCpu = new HashMap<>();
private final List<String> failures = new ArrayList<>();
Plans(HydrologySettings settings, HydrologyTerrainSource terrain, IntBinaryOperator naturalHeight, HydrologyWindows.WindowRange range) {
Plans(HydrologySettings settings, HydrologyTerrainSource terrain, NaturalHeights naturalHeight, HydrologyWindows.WindowRange range) {
this.settings = settings;
this.naturalHeight = naturalHeight;
this.planned = range.grown(1);
this.counting = new CountingTerrain(terrain);
this.planners = HydrologyPlanners.create(settings, counting);
this.rasterizer = new ChunkRasterizer(settings);
this.rasterizer = new ChunkRasterizer(settings, ChannelFluids.TIER);
Set<RegionKey> regionKeys = new TreeSet<>(REGION_ORDER);
for (WindowKey key : planned.keys()) {
regionKeys.addAll(WindowPlanner.lakeRegions(key));
@@ -11,6 +11,7 @@ import art.arcane.iris.generation.hydrology.plan.WindowPlan;
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.hydrology.raster.NaturalHeights;
import java.util.ArrayList;
import java.util.Collections;
@@ -27,7 +28,6 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.Future;
import java.util.function.IntBinaryOperator;
public final class HydrologyProbe {
private static final int RASTER_CACHE = 8192;
@@ -56,7 +56,7 @@ public final class HydrologyProbe {
}
}
record Inputs(HydrologySettings settings, HydrologyTerrainSource terrain, IntBinaryOperator naturalHeight, ForkJoinPool planningPool,
record Inputs(HydrologySettings settings, HydrologyTerrainSource terrain, NaturalHeights naturalHeight, ForkJoinPool planningPool,
ExecutorService workers, HydrologyVoxels.ChunkSource blocks, HydrologyUnderground.Source underground, int worldHeight) {
Inputs {
Objects.requireNonNull(settings, "settings");
@@ -98,7 +98,7 @@ public final class HydrologyProbe {
ForkJoinPool planningPool = MultiBurst.hydrology.submit((Callable<ForkJoinPool>) ForkJoinTask::getPool).get();
ExecutorService workers = Executors.newFixedThreadPool(configuration.threads());
try {
Inputs inputs = new Inputs(settings, terrain, terrain::naturalHeight, planningPool, workers,
Inputs inputs = new Inputs(settings, terrain, terrain::naturalSurface, planningPool, workers,
(chunkX, chunkZ) -> HydrologyVoxels.generate(engine, chunkX, chunkZ), HydrologyUnderground.generated(engine, workers),
engine.getTarget().getHeight());
HydrologyReport.Result measured = measure(inputs, target.windows());
@@ -8,15 +8,15 @@ import art.arcane.iris.generation.hydrology.raster.WaterRole;
import art.arcane.iris.generation.hydrology.water.Pond;
import art.arcane.iris.generation.hydrology.water.Reach;
import art.arcane.iris.generation.hydrology.water.ReachStyle;
import art.arcane.iris.generation.hydrology.raster.NaturalHeights;
import java.util.function.Consumer;
import java.util.function.IntBinaryOperator;
final class HydrologyRasterMetrics {
private final HydrologySettings settings;
private final HydrologyPlanMetrics plans;
private final Rasters rasters;
private final IntBinaryOperator naturalHeight;
private final NaturalHeights naturalHeight;
private final HydrologyWindows.WindowRange range;
private final Consumer<String> examples;
@@ -25,7 +25,7 @@ final class HydrologyRasterMetrics {
ChunkWaterRaster get(int chunkX, int chunkZ);
}
HydrologyRasterMetrics(HydrologySettings settings, HydrologyPlanMetrics plans, Rasters rasters, IntBinaryOperator naturalHeight,
HydrologyRasterMetrics(HydrologySettings settings, HydrologyPlanMetrics plans, Rasters rasters, NaturalHeights naturalHeight,
HydrologyWindows.WindowRange range, Consumer<String> examples) {
this.settings = settings;
this.plans = plans;
@@ -115,7 +115,7 @@ final class HydrologyRasterMetrics {
continue;
}
int terrain = raster.terrainHeight(x & 15, z & 15);
int natural = naturalHeight.applyAsInt(x, z);
int natural = (int) Math.round(naturalHeight.height(x, z));
if (terrain > natural + maxFill) {
overCap++;
examples.accept("berm raised " + x + "," + z + " from " + natural + " to " + terrain);
@@ -131,7 +131,7 @@ final class HydrologyRasterMetrics {
int terrain(int x, int z) {
ChunkWaterRaster raster = rasters.get(x >> 4, z >> 4);
return raster.empty() ? naturalHeight.applyAsInt(x, z) : raster.terrainHeight(x & 15, z & 15);
return raster.empty() ? (int) Math.round(naturalHeight.height(x, z)) : raster.terrainHeight(x & 15, z & 15);
}
private boolean seamMismatch(int x, int z, int nx, int nz) {
@@ -16,6 +16,7 @@ import art.arcane.iris.generation.hydrology.water.Reach;
import art.arcane.iris.generation.hydrology.water.ReachStyle;
import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.hydrology.raster.NaturalHeights;
import javax.imageio.ImageIO;
import java.awt.BasicStroke;
@@ -33,7 +34,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.function.IntBinaryOperator;
public final class RiverTransectProbe {
static final int NO_WATER = Integer.MIN_VALUE;
@@ -116,16 +116,16 @@ public final class RiverTransectProbe {
engine.getHeight());
IrisHydrologyTerrainSource terrain = new IrisHydrologyTerrainSource(engine.getComplex(), dimension, settings, MultiBurst.hydrology,
MultiBurst.hydrology.parallelism(), HydrologyBinding.UNBOUND);
HydrologyPlanHarness.Plans plans = new HydrologyPlanHarness.Plans(settings, terrain, terrain::naturalHeight, target.windows());
HydrologyPlanHarness.Plans plans = new HydrologyPlanHarness.Plans(settings, terrain, terrain::naturalSurface, target.windows());
List<Rendered> rendered = new ArrayList<>();
for (WindowKey key : target.windows().keys()) {
rendered.add(render(plans, key, terrain::naturalHeight, target.windows(), configuration.output()));
rendered.add(render(plans, key, terrain::naturalSurface, target.windows(), configuration.output()));
}
return rendered;
}
}
static Rendered render(HydrologyPlanHarness.Plans plans, WindowKey key, IntBinaryOperator natural, HydrologyWindows.WindowRange range,
static Rendered render(HydrologyPlanHarness.Plans plans, WindowKey key, NaturalHeights natural, HydrologyWindows.WindowRange range,
File output) throws Exception {
Map<Long, ChunkWaterRaster> rasters = new HashMap<>();
Columns columns = (x, z) -> {
@@ -134,7 +134,7 @@ public final class RiverTransectProbe {
}
ChunkWaterRaster raster = rasters.computeIfAbsent(HydrologyVoxels.chunkKey(x >> 4, z >> 4),
ignored -> plans.rasterize(x >> 4, z >> 4));
int ground = natural.applyAsInt(x, z);
int ground = (int) Math.round(natural.height(x, z));
return new Column(ground, raster.empty() ? ground : raster.terrainHeight(x & 15, z & 15), raster.waterHead(x & 15, z & 15),
raster.role(x & 15, z & 15));
};