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

@@ -22,6 +22,9 @@ public class IrisHydrology {
@Description("Fraction of eligible water features to keep after local placement probabilities. 0.5 keeps half the candidate river sources, ponds, lakes and underground water features without changing their configured sizes.") @Description("Fraction of eligible water features to keep after local placement probabilities. 0.5 keeps half the candidate river sources, ponds, lakes and underground water features without changing their configured sizes.")
private double density = 1D; private double density = 1D;
@Description("Wet and dry zones that decide where streams and ponds gather. Within a wet zone the configured density applies unchanged.")
private IrisHydrologyClimate climate = new IrisHydrologyClimate();
@Description("How water drains across the terrain before rivers and ponds are placed.") @Description("How water drains across the terrain before rivers and ponds are placed.")
private IrisHydrologyDrainage drainage = new IrisHydrologyDrainage(); private IrisHydrologyDrainage drainage = new IrisHydrologyDrainage();
@@ -0,0 +1,37 @@
package art.arcane.iris.generation.hydrology.config;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.noise.NoiseStyle;
import art.arcane.iris.pack.schema.annotation.MaxNumber;
import art.arcane.iris.pack.schema.annotation.MinNumber;
import art.arcane.volmlib.util.documentation.Description;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
@Accessors(chain = true)
@NoArgsConstructor
@Description("Wet and dry zones across the dimension. A noise field splits the land into zones: wet zones keep the rainfall and pond density of their biome, dry zones lose them. Streams and ponds then cluster in the wet zones instead of reaching every hill, and a river that rises in a wet zone still flows on through dry land.")
@Data
public class IrisHydrologyClimate {
@Description("Turns wet and dry zones on. When off, rainfall and pond density follow the hydrology policies alone.")
private boolean enabled = false;
@Description("Noise that lays out the zones. Land where its value is below dryness is dry. A larger zoom makes larger zones.")
private IrisGeneratorStyle style = NoiseStyle.SIMPLEX.style().zoomed(24D);
@MinNumber(0)
@MaxNumber(1)
@Description("Noise value below which land is dry. 0 keeps all land wet, and higher values dry out more of it.")
private double dryness = 0.5D;
@MinNumber(0)
@MaxNumber(1)
@Description("Width of the noise band over which rainfall fades from dry to wet. 0 gives sharp zone edges.")
private double transition = 0.1D;
@MinNumber(0)
@MaxNumber(1)
@Description("Share of the area's rainfall and pond density kept in dry zones. 0 lets dry zones start no streams or ponds of their own.")
private double dryRainfall = 0D;
}
@@ -24,12 +24,12 @@ public class IrisHydrologyDeformation {
@MinNumber(0) @MinNumber(0)
@MaxNumber(16) @MaxNumber(16)
@Description("How much steeper banks become on steep terrain.") @Description("How much steeper a bank stays where the natural ground between it and the water is steep.")
private double bankSteepFactor = 3.5D; private double bankSteepFactor = 3.5D;
@MinNumber(0) @MinNumber(0)
@MaxNumber(2) @MaxNumber(2)
@Description("Strength of the noise that roughens bank edges.") @Description("Strength of the smooth noise that roughens bank edges.")
private double bankNoise = 0.2D; private double bankNoise = 0.2D;
@Description("Keeps caves from opening under water, so rivers and ponds never drain into them.") @Description("Keeps caves from opening under water, so rivers and ponds never drain into them.")
@@ -31,13 +31,16 @@ public class IrisHydrologyPolicy {
@Description("Multiplier on how many ponds form in this area. With no value set anywhere it is 1.") @Description("Multiplier on how many ponds form in this area. With no value set anywhere it is 1.")
private Double pondDensity = null; private Double pondDensity = null;
@Description("Whether the dimension's wet and dry zones apply in this area. false keeps this area's full rainfall and pond density even inside a dry zone. With no value set anywhere they apply.")
private Boolean climateZones = null;
@Description("Whether streams and rivers may form in this area. With no value set anywhere they are allowed.") @Description("Whether streams and rivers may form in this area. With no value set anywhere they are allowed.")
private IrisHydrologyPermission streams = null; private IrisHydrologyPermission streams = null;
@Description("Whether ponds may form in this area. With no value set anywhere they are allowed.") @Description("Whether ponds may form in this area. With no value set anywhere they are allowed.")
private IrisHydrologyPermission ponds = null; private IrisHydrologyPermission ponds = null;
@Description("Name of a fluid profile that fills ponds and lakes whose floor lies in this area, instead of the ponds' own fluid. Channels always use the fluid of their tier.") @Description("Name of a fluid profile that fills the water in this area instead of its usual fluid: ponds and lakes whose floor lies here, and every stretch of stream or river that flows through here.")
private String fluid = null; private String fluid = null;
@RegistryListResource(IrisBiome.class) @RegistryListResource(IrisBiome.class)
@@ -14,8 +14,10 @@ import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureQuery; import art.arcane.iris.generation.hydrology.query.HydrologyFeatureQuery;
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureRef; import art.arcane.iris.generation.hydrology.query.HydrologyFeatureRef;
import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType; import art.arcane.iris.generation.hydrology.query.HydrologyFeatureType;
import art.arcane.iris.generation.hydrology.raster.ChannelFluids;
import art.arcane.iris.generation.hydrology.raster.ChunkRasterizer; import art.arcane.iris.generation.hydrology.raster.ChunkRasterizer;
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster; import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
import art.arcane.iris.generation.hydrology.raster.NaturalHeights;
import art.arcane.iris.generation.hydrology.underground.SeaInlet; import art.arcane.iris.generation.hydrology.underground.SeaInlet;
import art.arcane.iris.generation.hydrology.underground.SeaInletRasterizer; import art.arcane.iris.generation.hydrology.underground.SeaInletRasterizer;
import art.arcane.iris.generation.hydrology.water.HydrologyTier; import art.arcane.iris.generation.hydrology.water.HydrologyTier;
@@ -51,7 +53,6 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BooleanSupplier; import java.util.function.BooleanSupplier;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.IntBinaryOperator;
import java.util.function.LongSupplier; import java.util.function.LongSupplier;
import java.util.function.Predicate; import java.util.function.Predicate;
import java.util.function.Supplier; import java.util.function.Supplier;
@@ -68,7 +69,7 @@ public final class HydrologyRuntime {
private final HydrologySettings settings; private final HydrologySettings settings;
private final HydrologyPlanCache cache; private final HydrologyPlanCache cache;
private final ChunkRasterizer rasterizer; private final ChunkRasterizer rasterizer;
private final IntBinaryOperator naturalHeight; private final NaturalHeights naturalHeight;
private final BooleanSupplier mainThread; private final BooleanSupplier mainThread;
private final RasterLru rasters; private final RasterLru rasters;
private final ThreadLocal<LastRaster> last; private final ThreadLocal<LastRaster> last;
@@ -109,17 +110,17 @@ public final class HydrologyRuntime {
this.caveFluidPolicies = false; this.caveFluidPolicies = false;
} }
public HydrologyRuntime(HydrologySettings settings, HydrologyPlanCache cache, IntBinaryOperator naturalHeight, public HydrologyRuntime(HydrologySettings settings, HydrologyPlanCache cache, NaturalHeights naturalHeight,
BooleanSupplier mainThread, LongSupplier ticker) { BooleanSupplier mainThread, LongSupplier ticker) {
this(settings, cache, naturalHeight, mainThread, ticker, HydrologyBinding.UNBOUND, identity -> null, false); this(settings, cache, naturalHeight, ChannelFluids.TIER, mainThread, ticker, HydrologyBinding.UNBOUND, identity -> null, false);
} }
private HydrologyRuntime(HydrologySettings settings, HydrologyPlanCache cache, IntBinaryOperator naturalHeight, private HydrologyRuntime(HydrologySettings settings, HydrologyPlanCache cache, NaturalHeights naturalHeight,
BooleanSupplier mainThread, LongSupplier ticker, HydrologyBinding binding, ChannelFluids channelFluids, BooleanSupplier mainThread, LongSupplier ticker, HydrologyBinding binding,
Function<PreparedHydrologyCacheIdentity, HydrologyPlanStore> stores, boolean caveFluidPolicies) { Function<PreparedHydrologyCacheIdentity, HydrologyPlanStore> stores, boolean caveFluidPolicies) {
this.settings = Objects.requireNonNull(settings, "settings"); this.settings = Objects.requireNonNull(settings, "settings");
this.cache = Objects.requireNonNull(cache, "cache"); this.cache = Objects.requireNonNull(cache, "cache");
this.rasterizer = new ChunkRasterizer(settings); this.rasterizer = new ChunkRasterizer(settings, channelFluids);
this.naturalHeight = Objects.requireNonNull(naturalHeight, "naturalHeight"); this.naturalHeight = Objects.requireNonNull(naturalHeight, "naturalHeight");
this.mainThread = Objects.requireNonNull(mainThread, "mainThread"); this.mainThread = Objects.requireNonNull(mainThread, "mainThread");
this.rasters = new RasterLru(); this.rasters = new RasterLru();
@@ -160,7 +161,7 @@ public final class HydrologyRuntime {
HydrologyPlanCache cache = HydrologyPlanCache.create(settings, planners.regions(), planners.windows(), MultiBurst.hydrology, mainThread); HydrologyPlanCache cache = HydrologyPlanCache.create(settings, planners.regions(), planners.windows(), MultiBurst.hydrology, mainThread);
String dimensionKey = dimension.getLoadKey(); String dimensionKey = dimension.getLoadKey();
IrisWorld world = engine.getWorld(); IrisWorld world = engine.getWorld();
return new HydrologyRuntime(settings, cache, source::naturalHeight, mainThread, System::nanoTime, binding, return new HydrologyRuntime(settings, cache, source::naturalSurface, source, mainThread, System::nanoTime, binding,
identity -> store(world, dimensionKey, settings, identity), caveFluidPolicies); identity -> store(world, dimensionKey, settings, identity), caveFluidPolicies);
} }
@@ -7,11 +7,13 @@ import art.arcane.iris.generation.hydrology.config.IrisHydrologyPolicy;
import art.arcane.iris.generation.hydrology.grid.HydrologySampleBuffer; import art.arcane.iris.generation.hydrology.grid.HydrologySampleBuffer;
import art.arcane.iris.generation.hydrology.grid.HydrologyTerrainSource; import art.arcane.iris.generation.hydrology.grid.HydrologyTerrainSource;
import art.arcane.iris.generation.hydrology.plan.HydrologySettings; import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
import art.arcane.iris.generation.hydrology.raster.ChannelFluids;
import art.arcane.iris.generation.runtime.IrisComplex; import art.arcane.iris.generation.runtime.IrisComplex;
import art.arcane.iris.generation.terrain.InferredType; import art.arcane.iris.generation.terrain.InferredType;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.terrain.IrisRegion; import art.arcane.iris.generation.terrain.IrisRegion;
import art.arcane.iris.studio.generation.BiomeBuffetLayout; import art.arcane.iris.studio.generation.BiomeBuffetLayout;
import art.arcane.volmlib.util.noise.CNG;
import art.arcane.volmlib.util.stream.ProceduralStream; import art.arcane.volmlib.util.stream.ProceduralStream;
import java.util.ArrayList; import java.util.ArrayList;
@@ -25,9 +27,10 @@ import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function; import java.util.function.Function;
public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource { public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource, ChannelFluids {
private static final int MINIMUM_SLICE = 256; private static final int MINIMUM_SLICE = 256;
private static final int POLICY_CAPACITY = 4096; private static final int POLICY_CAPACITY = 4096;
private static final long SALT_CLIMATE = 0x434c494d415445L;
private final IrisComplex complex; private final IrisComplex complex;
private final IrisDimension dimension; private final IrisDimension dimension;
@@ -40,6 +43,8 @@ public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource
private final BiomeBuffetLayout buffet; private final BiomeBuffetLayout buffet;
private final IrisBiome focus; private final IrisBiome focus;
private final HydrologyBinding binding; private final HydrologyBinding binding;
private final HydrologySettings.Climate climate;
private final CNG wetness;
private final ConcurrentHashMap<PolicyKey, Policy> policies = new ConcurrentHashMap<>(); private final ConcurrentHashMap<PolicyKey, Policy> policies = new ConcurrentHashMap<>();
public IrisHydrologyTerrainSource(IrisComplex complex, IrisDimension dimension, HydrologySettings settings, ExecutorService pool, public IrisHydrologyTerrainSource(IrisComplex complex, IrisDimension dimension, HydrologySettings settings, ExecutorService pool,
@@ -55,6 +60,8 @@ public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource
this.buffet = complex.getBiomeBuffet(); this.buffet = complex.getBiomeBuffet();
this.focus = complex.getFocusBiome(); this.focus = complex.getFocusBiome();
this.binding = Objects.requireNonNull(binding, "binding"); this.binding = Objects.requireNonNull(binding, "binding");
this.climate = settings.climate();
this.wetness = climate.enabled() ? climate.style().create(settings.seed() ^ SALT_CLIMATE, complex.getData()) : null;
} }
@Override @Override
@@ -73,8 +80,13 @@ public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource
} }
} }
public int naturalHeight(int x, int z) { public double naturalSurface(int x, int z) {
return complex.naturalTrueHeight(x, z); return complex.naturalHeight(x, z);
}
@Override
public String channelFluid(int x, int z) {
return policy(biomes.get(x, z), regions.get(x, z)).fluid();
} }
@Override @Override
@@ -150,12 +162,17 @@ public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource
Policy policy = policy(biomes.get(x, z), regions.get(x, z)); Policy policy = policy(biomes.get(x, z), regions.get(x, z));
out.height()[index] = (float) height; out.height()[index] = (float) height;
out.ocean()[index] = StrictMath.round(height) < settings.seaLevel() && oceanIntent(x, z); out.ocean()[index] = StrictMath.round(height) < settings.seaLevel() && oceanIntent(x, z);
out.rainfall()[index] = policy.rainfall(); float wet = policy.climateZones() ? wetness(x, z) : 1f;
out.pondDensity()[index] = policy.pondDensity(); out.rainfall()[index] = policy.rainfall() * wet;
out.pondDensity()[index] = policy.pondDensity() * wet;
out.permission()[index] = policy.permission(); out.permission()[index] = policy.permission();
out.fluidProfile()[index] = policy.fluid(); out.fluidProfile()[index] = policy.fluid();
} }
private float wetness(int x, int z) {
return wetness == null ? 1f : (float) climate.wetness(wetness.noise(x, z));
}
private boolean oceanIntent(int x, int z) { private boolean oceanIntent(int x, int z) {
BiomeBuffetLayout.Cell cell = buffet == null ? null : buffet.terrain(x, z); BiomeBuffetLayout.Cell cell = buffet == null ? null : buffet.terrain(x, z);
IrisBiome focused = cell == null ? focus : cell.biome(); IrisBiome focused = cell == null ? focus : cell.biome();
@@ -183,13 +200,14 @@ public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource
IrisHydrologyPermission streams = inherited(biome, region, IrisHydrologyPolicy::getStreams, IrisHydrologyPermission.ALLOW); IrisHydrologyPermission streams = inherited(biome, region, IrisHydrologyPolicy::getStreams, IrisHydrologyPermission.ALLOW);
IrisHydrologyPermission ponds = inherited(biome, region, IrisHydrologyPolicy::getPonds, IrisHydrologyPermission.ALLOW); IrisHydrologyPermission ponds = inherited(biome, region, IrisHydrologyPolicy::getPonds, IrisHydrologyPermission.ALLOW);
String fluid = inherited(biome, region, IrisHydrologyPolicy::getFluid, null); String fluid = inherited(biome, region, IrisHydrologyPolicy::getFluid, null);
boolean climateZones = inherited(biome, region, IrisHydrologyPolicy::getClimateZones, Boolean.TRUE);
requireMultiplier("rainfall", rainfall, biome, region); requireMultiplier("rainfall", rainfall, biome, region);
requireMultiplier("pondDensity", pondDensity, biome, region); requireMultiplier("pondDensity", pondDensity, biome, region);
if (fluid != null && !settings.fluids().containsKey(fluid)) { if (fluid != null && !settings.fluids().containsKey(fluid)) {
throw new IllegalArgumentException("Hydrology policy for " + where(biome, region) + " names fluid '" + fluid throw new IllegalArgumentException("Hydrology policy for " + where(biome, region) + " names fluid '" + fluid
+ "', which is not one of the hydrology fluids " + settings.fluids().keySet()); + "', which is not one of the hydrology fluids " + settings.fluids().keySet());
} }
return new Policy((float) rainfall, (float) pondDensity, permission(streams, ponds), fluid); return new Policy((float) rainfall, (float) pondDensity, permission(streams, ponds), fluid, climateZones);
} }
private <T> T inherited(IrisBiome biome, IrisRegion region, Function<IrisHydrologyPolicy, T> field, T fallback) { private <T> T inherited(IrisBiome biome, IrisRegion region, Function<IrisHydrologyPolicy, T> field, T fallback) {
@@ -214,7 +232,7 @@ public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource
return "biome " + (biome == null ? "none" : biome.getLoadKey()) + " in region " + (region == null ? "none" : region.getLoadKey()); return "biome " + (biome == null ? "none" : biome.getLoadKey()) + " in region " + (region == null ? "none" : region.getLoadKey());
} }
private record Policy(float rainfall, float pondDensity, byte permission, String fluid) { private record Policy(float rainfall, float pondDensity, byte permission, String fluid, boolean climateZones) {
} }
private record PolicyKey(IrisBiome biome, IrisRegion region) { private record PolicyKey(IrisBiome biome, IrisRegion region) {
@@ -5,6 +5,7 @@ import art.arcane.iris.generation.decoration.IrisDecorationPart;
import art.arcane.iris.generation.decoration.IrisDecorator; import art.arcane.iris.generation.decoration.IrisDecorator;
import art.arcane.iris.generation.hydrology.config.IrisCavePools; import art.arcane.iris.generation.hydrology.config.IrisCavePools;
import art.arcane.iris.generation.hydrology.config.IrisFluidProfile; import art.arcane.iris.generation.hydrology.config.IrisFluidProfile;
import art.arcane.iris.generation.hydrology.config.IrisHydrologyClimate;
import art.arcane.iris.generation.hydrology.config.IrisHydrology; import art.arcane.iris.generation.hydrology.config.IrisHydrology;
import art.arcane.iris.generation.hydrology.config.IrisHydrologyDeformation; import art.arcane.iris.generation.hydrology.config.IrisHydrologyDeformation;
import art.arcane.iris.generation.hydrology.config.IrisHydrologyDrainage; import art.arcane.iris.generation.hydrology.config.IrisHydrologyDrainage;
@@ -20,6 +21,7 @@ import art.arcane.iris.generation.hydrology.config.IrisPondShore;
import art.arcane.iris.generation.hydrology.config.IrisSeaInlets; import art.arcane.iris.generation.hydrology.config.IrisSeaInlets;
import art.arcane.iris.generation.hydrology.underground.SeaInletRasterizer; import art.arcane.iris.generation.hydrology.underground.SeaInletRasterizer;
import art.arcane.iris.generation.hydrology.water.HydrologyTier; import art.arcane.iris.generation.hydrology.water.HydrologyTier;
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
import art.arcane.iris.generation.terrain.IrisMaterialPalette; import art.arcane.iris.generation.terrain.IrisMaterialPalette;
import com.google.gson.Gson; import com.google.gson.Gson;
@@ -51,6 +53,7 @@ public final class HydrologySettings {
private final int maxY; private final int maxY;
private final boolean enabled; private final boolean enabled;
private final float density; private final float density;
private final Climate climate;
private final Drainage drainage; private final Drainage drainage;
private final List<HydrologyTier> tiers; private final List<HydrologyTier> tiers;
private final Ponds ponds; private final Ponds ponds;
@@ -72,6 +75,7 @@ public final class HydrologySettings {
throw new IllegalArgumentException("Hydrology density must be within 0..1, got " + config.getDensity()); throw new IllegalArgumentException("Hydrology density must be within 0..1, got " + config.getDensity());
} }
this.density = (float) config.getDensity(); this.density = (float) config.getDensity();
this.climate = climate(config.getClimate());
this.drainage = drainage(config.getDrainage()); this.drainage = drainage(config.getDrainage());
this.tiers = tiers(config.getTiers()); this.tiers = tiers(config.getTiers());
this.ponds = ponds(config.getPonds()); this.ponds = ponds(config.getPonds());
@@ -135,6 +139,10 @@ public final class HydrologySettings {
return density; return density;
} }
public Climate climate() {
return climate;
}
public Drainage drainage() { public Drainage drainage() {
return drainage; return drainage;
} }
@@ -167,6 +175,22 @@ public final class HydrologySettings {
return persist; return persist;
} }
private static Climate climate(IrisHydrologyClimate config) {
Objects.requireNonNull(config, "Hydrology climate is missing");
IrisGeneratorStyle style = Objects.requireNonNull(config.getStyle(), "Hydrology climate.style is missing");
float dryness = unit(config.getDryness(), "climate.dryness");
float transition = unit(config.getTransition(), "climate.transition");
float dryRainfall = unit(config.getDryRainfall(), "climate.dryRainfall");
return new Climate(config.isEnabled(), style, dryness, transition, dryRainfall);
}
private static float unit(double value, String field) {
if (!Double.isFinite(value) || value < 0D || value > 1D) {
throw new IllegalArgumentException("Hydrology " + field + " must be within 0..1, got " + value);
}
return (float) value;
}
private static Drainage drainage(IrisHydrologyDrainage config) { private static Drainage drainage(IrisHydrologyDrainage config) {
return new Drainage((float) config.getFillEpsilon(), config.getBreachDepth(), config.isWetnessThinning(), return new Drainage((float) config.getFillEpsilon(), config.getBreachDepth(), config.isWetnessThinning(),
(float) config.getMinimumHeadKeep(), (float) config.getHeadRelief(), (float) config.getSaturationCatchment()); (float) config.getMinimumHeadKeep(), (float) config.getHeadRelief(), (float) config.getSaturationCatchment());
@@ -468,6 +492,7 @@ public final class HydrologySettings {
private long computeFingerprint() { private long computeFingerprint() {
Fingerprint hash = new Fingerprint().add(seed).add(seaLevel).add(minY).add(maxY).add(enabled) Fingerprint hash = new Fingerprint().add(seed).add(seaLevel).add(minY).add(maxY).add(enabled)
.add(density).add(prefetchRings).add(persist); .add(density).add(prefetchRings).add(persist);
addClimate(hash);
addDrainage(hash); addDrainage(hash);
addTiers(hash); addTiers(hash);
addPonds(hash); addPonds(hash);
@@ -477,6 +502,12 @@ public final class HydrologySettings {
return hash.value(); return hash.value();
} }
private void addClimate(Fingerprint hash) {
if (climate.enabled()) {
hash.add(true).add(PALETTE_JSON.toJson(climate.style())).add(climate.dryness()).add(climate.transition()).add(climate.dryRainfall());
}
}
private void addDrainage(Fingerprint hash) { private void addDrainage(Fingerprint hash) {
hash.add(drainage.fillEpsilon()).add(drainage.breachDepth()).add(drainage.wetnessThinning()) hash.add(drainage.fillEpsilon()).add(drainage.breachDepth()).add(drainage.wetnessThinning())
.add(drainage.minimumHeadKeep()).add(drainage.headRelief()).add(drainage.saturationCatchment()); .add(drainage.minimumHeadKeep()).add(drainage.headRelief()).add(drainage.saturationCatchment());
@@ -535,6 +566,23 @@ public final class HydrologySettings {
public record IntRange(int min, int max) { public record IntRange(int min, int max) {
} }
public record Climate(boolean enabled, IrisGeneratorStyle style, float dryness, float transition, float dryRainfall) {
public double wetness(double noise) {
if (!enabled || dryness <= 0f) {
return 1D;
}
double half = 0.5D * transition;
double share;
if (half <= 0D) {
share = noise < dryness ? 0D : 1D;
} else {
double t = Math.clamp((noise - dryness + half) / transition, 0D, 1D);
share = t * t * (3D - 2D * t);
}
return dryRainfall + (1D - dryRainfall) * share;
}
}
public record Drainage(float fillEpsilon, int breachDepth, boolean wetnessThinning, float minimumHeadKeep, float headRelief, public record Drainage(float fillEpsilon, int breachDepth, boolean wetnessThinning, float minimumHeadKeep, float headRelief,
float saturationCatchment) { float saturationCatchment) {
} }
@@ -6,6 +6,7 @@ final class BedProfile {
private static final long SALT_BROAD = 0x42454442524f4144L; private static final long SALT_BROAD = 0x42454442524f4144L;
private static final long SALT_DETAIL = 0x4245444445544149L; private static final long SALT_DETAIL = 0x4245444445544149L;
private static final long SALT_TEXTURE = 0x4245445445585455L; private static final long SALT_TEXTURE = 0x4245445445585455L;
private static final long SALT_ROUGHNESS = 0x42414e4b524f5547L;
private BedProfile() { private BedProfile() {
} }
@@ -18,6 +19,10 @@ final class BedProfile {
return 0.35D * noise(seed, SALT_TEXTURE, x, z, 2); return 0.35D * noise(seed, SALT_TEXTURE, x, z, 2);
} }
static double roughness(long seed, int x, int z) {
return noise(seed, SALT_ROUGHNESS, x, z, 4) - 0.5D;
}
static double position(double u, double variation) { static double position(double u, double variation) {
double bounded = Math.clamp(u, 0D, 1D); double bounded = Math.clamp(u, 0D, 1D);
return bounded + 0.8D * variation * bounded * (1D - bounded); return bounded + 0.8D * variation * bounded * (1D - bounded);
@@ -0,0 +1,8 @@
package art.arcane.iris.generation.hydrology.raster;
@FunctionalInterface
public interface ChannelFluids {
ChannelFluids TIER = (x, z) -> null;
String channelFluid(int x, int z);
}
@@ -8,17 +8,18 @@ import art.arcane.iris.generation.hydrology.water.HydrologyTier;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.function.IntBinaryOperator;
public final class ChunkRasterizer { public final class ChunkRasterizer {
private static final int CHUNK = 16; private static final int CHUNK = 16;
private static final int CANDIDATE_HALO = 48; private static final int CANDIDATE_HALO = 48;
private final HydrologySettings settings; private final HydrologySettings settings;
private final ChannelFluids channelFluids;
private final int halo; private final int halo;
public ChunkRasterizer(HydrologySettings settings) { public ChunkRasterizer(HydrologySettings settings, ChannelFluids channelFluids) {
this.settings = Objects.requireNonNull(settings, "settings"); this.settings = Objects.requireNonNull(settings, "settings");
this.channelFluids = Objects.requireNonNull(channelFluids, "channelFluids");
float widest = 0f; float widest = 0f;
for (HydrologyTier tier : settings.tiers()) { for (HydrologyTier tier : settings.tiers()) {
widest = Math.max(widest, tier.maxHalfWidth()); widest = Math.max(widest, tier.maxHalfWidth());
@@ -33,13 +34,13 @@ public final class ChunkRasterizer {
chunkX * CHUNK + CHUNK - 1 + halo, chunkZ * CHUNK + CHUNK - 1 + halo); chunkX * CHUNK + CHUNK - 1 + halo, chunkZ * CHUNK + CHUNK - 1 + halo);
} }
public ChunkWaterRaster rasterize(int chunkX, int chunkZ, IntBinaryOperator naturalHeight, List<IndexedWindow> windows, public ChunkWaterRaster rasterize(int chunkX, int chunkZ, NaturalHeights naturalHeight, List<IndexedWindow> windows,
List<RegionPlan> lakeRegionPlans) { List<RegionPlan> lakeRegionPlans) {
Objects.requireNonNull(naturalHeight, "naturalHeight"); Objects.requireNonNull(naturalHeight, "naturalHeight");
ChunkFeatures features = ChunkFeatures.gather(settings, chunkX, chunkZ, halo, windows, lakeRegionPlans, lakeRegions(chunkX, chunkZ)); ChunkFeatures features = ChunkFeatures.gather(settings, chunkX, chunkZ, halo, windows, lakeRegionPlans, lakeRegions(chunkX, chunkZ));
if (features.inert()) { if (features.inert()) {
return ChunkWaterRaster.EMPTY; return ChunkWaterRaster.EMPTY;
} }
return new RasterComposer(settings, features, chunkX, chunkZ).compose(naturalHeight); return new RasterComposer(settings, features, channelFluids, chunkX, chunkZ).compose(naturalHeight);
} }
} }
@@ -0,0 +1,6 @@
package art.arcane.iris.generation.hydrology.raster;
@FunctionalInterface
public interface NaturalHeights {
double height(int x, int z);
}
@@ -2,7 +2,6 @@ package art.arcane.iris.generation.hydrology.raster;
import art.arcane.iris.generation.hydrology.config.IrisBankStyle; import art.arcane.iris.generation.hydrology.config.IrisBankStyle;
import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell; import art.arcane.iris.generation.hydrology.cave.HydrologyCaveCell;
import art.arcane.iris.generation.hydrology.grid.CellHash;
import art.arcane.iris.generation.hydrology.plan.HydrologySettings; import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
import art.arcane.iris.generation.hydrology.underground.SeaInlet; import art.arcane.iris.generation.hydrology.underground.SeaInlet;
import art.arcane.iris.generation.hydrology.water.HydrologyTier; import art.arcane.iris.generation.hydrology.water.HydrologyTier;
@@ -11,7 +10,6 @@ import art.arcane.iris.generation.hydrology.water.Reach;
import art.arcane.iris.generation.hydrology.water.ReachStyle; import art.arcane.iris.generation.hydrology.water.ReachStyle;
import java.util.Arrays; import java.util.Arrays;
import java.util.function.IntBinaryOperator;
final class RasterComposer { final class RasterComposer {
private static final int CHUNK = 16; private static final int CHUNK = 16;
@@ -22,12 +20,15 @@ final class RasterComposer {
private static final double MAX_CONE_SLOPE = 2D; private static final double MAX_CONE_SLOPE = 2D;
private static final double CONE_EXPONENT = 0.8D; private static final double CONE_EXPONENT = 0.8D;
private static final double ROUNDING = 1e-6D; private static final double ROUNDING = 1e-6D;
private static final long SALT_BANK = 0x42414e4b4e5aL; private static final int SLOPE_REACH = 2;
private static final int RAY_STEP = 4;
private final HydrologySettings settings; private final HydrologySettings settings;
private final ChunkFeatures features; private final ChunkFeatures features;
private final ChannelFluids channelFluids;
private final int originX; private final int originX;
private final int originZ; private final int originZ;
private final double[] exact = new double[CELLS];
private final int[] natural = new int[CELLS]; private final int[] natural = new int[CELLS];
private final int[] terrain = new int[CELLS]; private final int[] terrain = new int[CELLS];
private final int[] head = new int[CELLS]; private final int[] head = new int[CELLS];
@@ -41,11 +42,12 @@ final class RasterComposer {
private final HydrologyTier[] nearTier = new HydrologyTier[CELLS]; private final HydrologyTier[] nearTier = new HydrologyTier[CELLS];
private final ReachCursor cursor = new ReachCursor(); private final ReachCursor cursor = new ReachCursor();
private final int[] inletColumn = new int[2]; private final int[] inletColumn = new int[2];
private IntBinaryOperator naturalHeight; private NaturalHeights naturalHeight;
RasterComposer(HydrologySettings settings, ChunkFeatures features, int chunkX, int chunkZ) { RasterComposer(HydrologySettings settings, ChunkFeatures features, ChannelFluids channelFluids, int chunkX, int chunkZ) {
this.settings = settings; this.settings = settings;
this.features = features; this.features = features;
this.channelFluids = channelFluids;
this.originX = chunkX * CHUNK - 1; this.originX = chunkX * CHUNK - 1;
this.originZ = chunkZ * CHUNK - 1; this.originZ = chunkZ * CHUNK - 1;
Arrays.fill(head, Integer.MIN_VALUE); Arrays.fill(head, Integer.MIN_VALUE);
@@ -54,11 +56,12 @@ final class RasterComposer {
Arrays.fill(distance, Double.POSITIVE_INFINITY); Arrays.fill(distance, Double.POSITIVE_INFINITY);
} }
ChunkWaterRaster compose(IntBinaryOperator naturalHeight) { ChunkWaterRaster compose(NaturalHeights naturalHeight) {
this.naturalHeight = naturalHeight; this.naturalHeight = naturalHeight;
for (int cell = 0; cell < CELLS; cell++) { for (int cell = 0; cell < CELLS; cell++) {
if (!corner(cell)) { if (!corner(cell)) {
natural[cell] = naturalHeight.applyAsInt(originX + cell % GRID, originZ + cell / GRID); exact[cell] = naturalHeight.height(originX + cell % GRID, originZ + cell / GRID);
natural[cell] = (int) Math.round(exact[cell]);
terrain[cell] = natural[cell]; terrain[cell] = natural[cell];
} }
} }
@@ -107,7 +110,7 @@ final class RasterComposer {
return; return;
} }
if (cursor.channel.reach != null) { if (cursor.channel.reach != null) {
channel(cell, ground, variation, BedProfile.detail(settings.seed(), x, z)); channel(cell, x, z, ground, variation);
} else if (interior(cell)) { } else if (interior(cell)) {
dry(cell, x, z, ground, variation); dry(cell, x, z, ground, variation);
} }
@@ -129,16 +132,17 @@ final class RasterComposer {
return Math.min(floor, bed(hit, Math.min(level, level(hit.reach.grade()[hit.point()])), ground, variation, detail)); return Math.min(floor, bed(hit, Math.min(level, level(hit.reach.grade()[hit.point()])), ground, variation, detail));
} }
private void channel(int cell, int ground, double variation, double detail) { private void channel(int cell, int x, int z, int ground, double variation) {
ReachCursor.Choice hit = cursor.channel; ReachCursor.Choice hit = cursor.channel;
Reach reach = hit.reach; Reach reach = hit.reach;
int point = hit.point(); int point = hit.point();
String area = channelFluids.channelFluid(x, z);
head[cell] = level(reach.grade()[point]); head[cell] = level(reach.grade()[point]);
terrain[cell] = bed(hit, head[cell], ground, variation, detail); terrain[cell] = bed(hit, head[cell], ground, variation, BedProfile.detail(settings.seed(), x, z));
role[cell] = role(reach.style()[point]); role[cell] = role(reach.style()[point]);
feature[cell] = reach.id(); feature[cell] = reach.id();
tier[cell] = reach.tier(); tier[cell] = reach.tier();
fluid[cell] = reach.tier().fluid(); fluid[cell] = area != null ? area : reach.tier().fluid();
} }
private static int bed(ReachCursor.Choice hit, int surface, int ground, double variation, double detail) { private static int bed(ReachCursor.Choice hit, int surface, int ground, double variation, double detail) {
@@ -162,7 +166,8 @@ final class RasterComposer {
u = BedProfile.position(u, -variation); u = BedProfile.position(u, -variation);
} }
if (ground >= surface) { if (ground >= surface) {
cut = BankProfile.cut(bankTier.bankStyle(), surface, ground, u, coneSlope(cell) * u * bankTier.bankBand(), noise(x, z)); double cone = coneSlope(steepestToWater(x, z, bank, bankEdge)) * u * bankTier.bankBand();
cut = BankProfile.cut(bankTier.bankStyle(), surface, ground, u, cone, noise(x, z));
} else { } else {
raise = BankProfile.levee(bankTier.bankStyle(), surface, ground, u, noise(x, z), settings.deformation().maxFill()); raise = BankProfile.levee(bankTier.bankStyle(), surface, ground, u, noise(x, z), settings.deformation().maxFill());
} }
@@ -205,15 +210,47 @@ final class RasterComposer {
distance[cell] = best; distance[cell] = best;
} }
private double coneSlope(int cell) { private double coneSlope(double slope) {
double slope = Math.max(Math.abs(natural[cell + 1] - natural[cell - 1]), Math.abs(natural[cell + GRID] - natural[cell - GRID])) / 2D;
HydrologySettings.Deformation deformation = settings.deformation(); HydrologySettings.Deformation deformation = settings.deformation();
return Math.min(MAX_CONE_SLOPE, deformation.bankSlope() + deformation.bankSteepFactor() * Math.pow(slope, CONE_EXPONENT)); return Math.min(MAX_CONE_SLOPE, deformation.bankSlope() + deformation.bankSteepFactor() * Math.pow(slope, CONE_EXPONENT));
} }
private double steepestToWater(int x, int z, ReachCursor.Choice bank, double bankEdge) {
Reach reach = bank.reach;
int next = Math.min(bank.segment + 1, reach.xs().length - 1);
double centreX = reach.xs()[bank.segment] + (reach.xs()[next] - reach.xs()[bank.segment]) * bank.t;
double centreZ = reach.zs()[bank.segment] + (reach.zs()[next] - reach.zs()[bank.segment]) * bank.t;
double steepest = slopeAt(x, z);
double length = Math.hypot(centreX - x, centreZ - z);
if (length <= 0D) {
return steepest;
}
double towardX = (centreX - x) / length;
double towardZ = (centreZ - z) / length;
for (double edge = 0D; edge < bankEdge; edge += RAY_STEP) {
double back = bankEdge - edge;
steepest = Math.max(steepest, slopeAt((int) Math.floor(x + towardX * back + 0.5D), (int) Math.floor(z + towardZ * back + 0.5D)));
}
return steepest;
}
private double slopeAt(int x, int z) {
return Math.max(Math.abs(exactAt(x + SLOPE_REACH, z) - exactAt(x - SLOPE_REACH, z)),
Math.abs(exactAt(x, z + SLOPE_REACH) - exactAt(x, z - SLOPE_REACH))) / (2D * SLOPE_REACH);
}
private double exactAt(int x, int z) {
int gx = x - originX;
int gz = z - originZ;
if (gx >= 0 && gz >= 0 && gx < GRID && gz < GRID && !corner(gz * GRID + gx)) {
return exact[gz * GRID + gx];
}
return naturalHeight.height(x, z);
}
private double noise(int x, int z) { private double noise(int x, int z) {
float strength = settings.deformation().bankNoise(); float strength = settings.deformation().bankNoise();
return strength == 0f ? 0D : (CellHash.unit(CellHash.mix(settings.seed(), SALT_BANK, x, z)) - 0.5D) * strength; return strength == 0f ? 0D : BedProfile.roughness(settings.seed(), x, z) * strength;
} }
private static int taperCut(int level, int ground, double u) { private static int taperCut(int level, int ground, double u) {
@@ -349,7 +386,7 @@ final class RasterComposer {
if (gx >= 0 && gz >= 0 && gx < GRID && gz < GRID && !corner(gz * GRID + gx)) { if (gx >= 0 && gz >= 0 && gx < GRID && gz < GRID && !corner(gz * GRID + gx)) {
return natural[gz * GRID + gx]; return natural[gz * GRID + gx];
} }
return naturalHeight.applyAsInt(x, z); return (int) Math.round(naturalHeight.height(x, z));
} }
private void nameShores() { private void nameShores() {
@@ -1680,7 +1680,11 @@ public class IrisComplex implements DataProvider {
/** The natural terrain height of a column, from the natural height stream only: never touches hydrology or its caches. */ /** The natural terrain height of a column, from the natural height stream only: never touches hydrology or its caches. */
public int naturalTrueHeight(int x, int z) { public int naturalTrueHeight(int x, int z) {
return (int) Math.round(naturalHeightStream.getDouble(x, z)); return (int) Math.round(naturalHeight(x, z));
}
public double naturalHeight(int x, int z) {
return naturalHeightStream.getDouble(x, z);
} }
/** The terrain slope of a column from the natural height stream only: never touches hydrology or its caches. */ /** The terrain slope of a column from the natural height stream only: never touches hydrology or its caches. */
@@ -129,6 +129,52 @@ public class IrisHydrologyTerrainSourceTest {
} }
} }
@Test
public void climateZonesDryOutRainfallAndPondsOutsideOptedOutBiomes() throws Exception {
Path pack = EngineFixture.pack(temporaryFolder.newFolder().toPath());
int[] xs = new int[COLUMNS];
int[] zs = new int[COLUMNS];
columns(xs, zs);
try (EngineFixture.Environment ignored = EngineFixture.environment(); EngineFixture fixture = new EngineFixture(pack)) {
IrisHydrology config = new IrisHydrology().setEnabled(true).setPersist(false);
config.getClimate().setEnabled(true).setDryness(0.5D).setTransition(0D).getStyle().setZoom(4D);
HydrologySettings settings = HydrologySettings.compile(config, EngineFixture.SEED, fixture.dimension.getFluidHeight(), 0,
EngineFixture.HEIGHT);
fixture.data.getBiomeLoader().load("meadow").getHydrologyPolicy().setClimateZones(false);
IrisHydrologyTerrainSource source = new IrisHydrologyTerrainSource(fixture.complex, fixture.dimension, settings, null, 1,
HydrologyBinding.UNBOUND);
HydrologySampleBuffer samples = new HydrologySampleBuffer(COLUMNS);
source.sample(xs, zs, samples, false);
int dry = 0;
int wet = 0;
int kept = 0;
for (int index = 0; index < COLUMNS; index++) {
int x = xs[index];
int z = zs[index];
boolean meadow = fixture.complex.getBaseBiomeStream().get(x, z).getLoadKey().equals("meadow");
boolean alpha = fixture.complex.getRegionStream().get(x, z).getLoadKey().equals("alpha");
String at = x + "," + z;
assertEquals(at, alpha ? "water" : null, source.channelFluid(x, z));
if (meadow) {
assertEquals(at, 2f, samples.rainfall()[index], 0f);
assertEquals(at, alpha ? 3f : 1f, samples.pondDensity()[index], 0f);
kept++;
continue;
}
boolean zoneDry = samples.rainfall()[index] == 0f;
assertEquals(at, zoneDry ? 0f : 0.5f, samples.rainfall()[index], 0f);
assertEquals(at, zoneDry ? 0f : alpha ? 3f : 1f, samples.pondDensity()[index], 0f);
if (zoneDry) {
dry++;
} else {
wet++;
}
}
assertTrue("dry columns " + dry + ", wet columns " + wet + ", opted-out columns " + kept,
dry > COLUMNS / 20 && wet > COLUMNS / 20 && kept > 0);
}
}
private static void assertUnknownFluidRejected(EngineFixture fixture, HydrologySettings settings, int[] xs, int[] zs) { private static void assertUnknownFluidRejected(EngineFixture fixture, HydrologySettings settings, int[] xs, int[] zs) {
IrisRegion beta = fixture.data.getRegionLoader().load("beta"); IrisRegion beta = fixture.data.getRegionLoader().load("beta");
beta.setHydrologyPolicy(new IrisHydrologyPolicy().setFluid("slime")); beta.setHydrologyPolicy(new IrisHydrologyPolicy().setFluid("slime"));
@@ -48,6 +48,55 @@ public class HydrologySettingsTest {
@Rule @Rule
public final TemporaryFolder temporary = new TemporaryFolder(); public final TemporaryFolder temporary = new TemporaryFolder();
@Test
public void climateIsBoundedAndOnlyEnabledZonesChangeThePlanIdentity() {
HydrologySettings plain = compile(new IrisHydrology());
IrisHydrology disabled = new IrisHydrology();
disabled.getClimate().setDryness(0.8D).setTransition(0.3D);
assertEquals(plain.fingerprint(), compile(disabled).fingerprint());
IrisHydrology zoned = new IrisHydrology();
zoned.getClimate().setEnabled(true);
IrisHydrology drier = new IrisHydrology();
drier.getClimate().setEnabled(true).setDryness(0.6D);
IrisHydrology larger = new IrisHydrology();
larger.getClimate().setEnabled(true).getStyle().setZoom(48D);
Set<Long> fingerprints = new HashSet<>(List.of(plain.fingerprint(), compile(zoned).fingerprint(), compile(drier).fingerprint(),
compile(larger).fingerprint()));
assertEquals(4, fingerprints.size());
for (double invalid : new double[]{-0.01D, 1.01D, Double.NaN}) {
IrisHydrology dryness = new IrisHydrology();
dryness.getClimate().setDryness(invalid);
assertRejected(dryness, "climate.dryness");
IrisHydrology transition = new IrisHydrology();
transition.getClimate().setTransition(invalid);
assertRejected(transition, "climate.transition");
IrisHydrology dryRainfall = new IrisHydrology();
dryRainfall.getClimate().setDryRainfall(invalid);
assertRejected(dryRainfall, "climate.dryRainfall");
}
}
@Test
public void climateWetnessFadesFromDryToWetZones() {
HydrologySettings.Climate off = compile(new IrisHydrology()).climate();
assertEquals(1D, off.wetness(0D), 0D);
IrisHydrology sharp = new IrisHydrology();
sharp.getClimate().setEnabled(true).setDryness(0.5D).setTransition(0D);
HydrologySettings.Climate edge = compile(sharp).climate();
assertEquals(0D, edge.wetness(0.49D), 0D);
assertEquals(1D, edge.wetness(0.5D), 0D);
IrisHydrology soft = new IrisHydrology();
soft.getClimate().setEnabled(true).setDryness(0.5D).setTransition(0.2D).setDryRainfall(0.25D);
HydrologySettings.Climate faded = compile(soft).climate();
assertEquals(0.25D, faded.wetness(0.3D), 1e-9D);
assertEquals(0.625D, faded.wetness(0.5D), 1e-9D);
assertEquals(1D, faded.wetness(0.7D), 1e-9D);
assertTrue(faded.wetness(0.45D) < faded.wetness(0.55D));
IrisHydrology wet = new IrisHydrology();
wet.getClimate().setEnabled(true).setDryness(0D);
assertEquals(1D, compile(wet).climate().wetness(0D), 0D);
}
@Test @Test
public void tierForPicksHighestTierBelowCatchment() { public void tierForPicksHighestTierBelowCatchment() {
HydrologySettings settings = compile(new IrisHydrology()); HydrologySettings settings = compile(new IrisHydrology());
@@ -261,7 +261,7 @@ public class ChunkWaterRasterTest {
assertTrue("the chain ends at or below the first end grade", previous <= 60); assertTrue("the chain ends at or below the first end grade", previous <= 60);
List<IndexedWindow> reversed = new ArrayList<>(scene.windows(31, 34)); List<IndexedWindow> reversed = new ArrayList<>(scene.windows(31, 34));
Collections.reverse(reversed); Collections.reverse(reversed);
ChunkWaterRaster backward = new ChunkRasterizer(scene.settings).rasterize(31, 34, scene.natural, reversed, scene.regions(31, 34)); ChunkWaterRaster backward = new ChunkRasterizer(scene.settings, ChannelFluids.TIER).rasterize(31, 34, scene.natural::applyAsInt, reversed, scene.regions(31, 34));
for (int lz = 0; lz < 16; lz++) { for (int lz = 0; lz < 16; lz++) {
for (int lx = 0; lx < 16; lx++) { for (int lx = 0; lx < 16; lx++) {
assertEquals(scene.raster(31, 34).waterHead(lx, lz), backward.waterHead(lx, lz)); assertEquals(scene.raster(31, 34).waterHead(lx, lz), backward.waterHead(lx, lz));
@@ -403,7 +403,7 @@ public class ChunkWaterRasterTest {
SeaInlet inlet = new SeaInlet(71L, xs, zs, halfWidth, SEA_LEVEL, floor, ceiling, 64, 8, 6); SeaInlet inlet = new SeaInlet(71L, xs, zs, halfWidth, SEA_LEVEL, floor, ceiling, 64, 8, 6);
WindowKey key = new WindowKey(0, 0); WindowKey key = new WindowKey(0, 0);
IndexedWindow own = new IndexedWindow(new WindowPlan(key, List.of(), List.of(), List.of(inlet)), ReachIndex.build(List.of(), List.of())); IndexedWindow own = new IndexedWindow(new WindowPlan(key, List.of(), List.of(), List.of(inlet)), ReachIndex.build(List.of(), List.of()));
ChunkRasterizer rasterizer = new ChunkRasterizer(settings); ChunkRasterizer rasterizer = new ChunkRasterizer(settings, ChannelFluids.TIER);
ChunkWaterRaster mouth = rasterizer.rasterize(0, 0, (x, z) -> 70, List.of(own), regions(0, 0, List.of())); ChunkWaterRaster mouth = rasterizer.rasterize(0, 0, (x, z) -> 70, List.of(own), regions(0, 0, List.of()));
int[] column = new int[2]; int[] column = new int[2];
int open = 0; int open = 0;
@@ -440,8 +440,8 @@ public class ChunkWaterRasterTest {
samples.incrementAndGet(); samples.incrementAndGet();
return 70; return 70;
}; };
ChunkRasterizer rasterizer = new ChunkRasterizer(settings); ChunkRasterizer rasterizer = new ChunkRasterizer(settings, ChannelFluids.TIER);
ChunkWaterRaster raster = rasterizer.rasterize(0, 0, natural, List.of(own), regions(0, 0, List.of())); ChunkWaterRaster raster = rasterizer.rasterize(0, 0, natural::applyAsInt, List.of(own), regions(0, 0, List.of()));
assertTrue(raster.empty()); assertTrue(raster.empty());
assertFalse(raster.provisional()); assertFalse(raster.provisional());
assertEquals("a riverless chunk never samples natural heights", 0, samples.get()); assertEquals("a riverless chunk never samples natural heights", 0, samples.get());
@@ -459,9 +459,94 @@ public class ChunkWaterRasterTest {
assertThrows(IllegalStateException.class, () -> raster.terrainHeight(0, 0)); assertThrows(IllegalStateException.class, () -> raster.terrainHeight(0, 0));
assertTrue(ChunkWaterRaster.PROVISIONAL.provisional()); assertTrue(ChunkWaterRaster.PROVISIONAL.provisional());
assertTrue(ChunkWaterRaster.PROVISIONAL.empty()); assertTrue(ChunkWaterRaster.PROVISIONAL.empty());
assertThrows(IllegalArgumentException.class, () -> rasterizer.rasterize(40, 0, natural, List.of(own), regions(40, 0, List.of()))); assertThrows(IllegalArgumentException.class, () -> rasterizer.rasterize(40, 0, natural::applyAsInt, List.of(own), regions(40, 0, List.of())));
assertFalse("a chunk on the reach is not empty", assertFalse("a chunk on the reach is not empty",
rasterizer.rasterize(27, 25, natural, List.of(own), regions(27, 25, List.of())).empty()); rasterizer.rasterize(27, 25, natural::applyAsInt, List.of(own), regions(27, 25, List.of())).empty());
}
@Test
public void channelTakesTheAreaFluidWhereOneIsSet() {
HydrologySettings settings = settings(new IrisHydrology());
Reach reach = line(61L, tier("stream", 1.5f, 0.5f, 0, IrisBankStyle.SMOOTH), 0L, 400f, 400f, 480f, 400f, (x, z) -> 69);
WindowKey key = new WindowKey(0, 0);
IndexedWindow own = new IndexedWindow(new WindowPlan(key, List.of(reach), List.of(), List.of()), ReachIndex.build(List.of(reach), List.of()));
ChannelFluids volcanic = (x, z) -> x < 440 ? "lava" : null;
ChunkWaterRaster raster = new ChunkRasterizer(settings, volcanic).rasterize(27, 25, (x, z) -> 70, List.of(own), regions(27, 25, List.of()));
int lava = 0;
int water = 0;
for (int lz = 0; lz < 16; lz++) {
for (int lx = 0; lx < 16; lx++) {
if (!isChannel(raster.role(lx, lz))) {
continue;
}
boolean inside = 27 * 16 + lx < 440;
assertEquals("column " + lx + "," + lz, inside ? "lava" : "water", raster.fluid(lx, lz));
if (inside) {
lava++;
} else {
water++;
}
}
}
assertTrue("lava columns " + lava + ", water columns " + water, lava > 0 && water > 0);
}
@Test
public void banksNeverDipAwayFromTheWater() {
HydrologySettings settings = settings(new IrisHydrology());
Reach river = line(81L, tier("river", 3f, 0.5f, 32, IrisBankStyle.SMOOTH), 0L, 96f, 256f, 416f, 256f, (x, z) -> 69);
WindowKey key = new WindowKey(0, 0);
IndexedWindow own = new IndexedWindow(new WindowPlan(key, List.of(river), List.of(), List.of()), ReachIndex.build(List.of(river), List.of()));
NaturalHeights natural = (x, z) -> {
double away = Math.abs(z - 256);
if (x < 256) {
return 70D + 8D * Math.min(1D, away / 8D) + 0.15D * Math.max(0D, away - 8D);
}
return 70D + 8D * (1D - Math.exp(-away / 6D)) + (x < 336 ? 0D : 0.15D * away);
};
ChunkRasterizer rasterizer = new ChunkRasterizer(settings, ChannelFluids.TIER);
Map<Long, Integer> terrain = new HashMap<>();
Map<Long, WaterRole> roles = new HashMap<>();
for (int chunkZ = 13; chunkZ <= 18; chunkZ++) {
for (int chunkX = 12; chunkX <= 25; chunkX++) {
ChunkWaterRaster raster = rasterizer.rasterize(chunkX, chunkZ, natural, List.of(own), regions(chunkX, chunkZ, List.of()));
for (int lz = 0; lz < 16; lz++) {
for (int lx = 0; lx < 16; lx++) {
int x = chunkX * 16 + lx;
int z = chunkZ * 16 + lz;
terrain.put(column(x, z), raster.empty() ? (int) Math.round(natural.height(x, z)) : raster.terrainHeight(lx, lz));
roles.put(column(x, z), raster.empty() ? WaterRole.NONE : raster.role(lx, lz));
}
}
}
}
int cut = 0;
List<String> pits = new ArrayList<>();
for (int z = 13 * 16 + 1; z < 19 * 16 - 1; z++) {
for (int x = 12 * 16 + 1; x < 26 * 16 - 1; x++) {
if (roles.get(column(x, z)) != WaterRole.BANK) {
continue;
}
int height = terrain.get(column(x, z));
if (height < (int) Math.round(natural.height(x, z))) {
cut++;
}
int inward = z > 256 ? z - 1 : z + 1;
if (z == 256 || roles.get(column(x, inward)) != WaterRole.BANK) {
continue;
}
int nearer = terrain.get(column(x, inward));
if (height < nearer) {
pits.add(x + "," + z + "@" + height + "<" + nearer);
}
}
}
assertTrue("cut bank columns " + cut, cut > 400);
assertTrue("bank dips " + pits.size() + " " + pits.subList(0, Math.min(10, pits.size())), pits.isEmpty());
}
private static long column(int x, int z) {
return ((long) x << 32) | (z & 0xFFFFFFFFL);
} }
@Test @Test
@@ -525,7 +610,7 @@ public class ChunkWaterRasterTest {
ChunkWaterRaster forward = scene.raster(31, 12); ChunkWaterRaster forward = scene.raster(31, 12);
List<IndexedWindow> reversed = new ArrayList<>(scene.windows(31, 12)); List<IndexedWindow> reversed = new ArrayList<>(scene.windows(31, 12));
Collections.reverse(reversed); Collections.reverse(reversed);
ChunkWaterRaster backward = new ChunkRasterizer(scene.settings).rasterize(31, 12, scene.natural, reversed, scene.regions(31, 12)); ChunkWaterRaster backward = new ChunkRasterizer(scene.settings, ChannelFluids.TIER).rasterize(31, 12, scene.natural::applyAsInt, reversed, scene.regions(31, 12));
for (int lz = 0; lz < 16; lz++) { for (int lz = 0; lz < 16; lz++) {
for (int lx = 0; lx < 16; lx++) { for (int lx = 0; lx < 16; lx++) {
assertEquals(forward.waterHead(lx, lz), backward.waterHead(lx, lz)); assertEquals(forward.waterHead(lx, lz), backward.waterHead(lx, lz));
@@ -837,7 +922,7 @@ public class ChunkWaterRasterTest {
private Scene(HydrologySettings settings, IntBinaryOperator natural) { private Scene(HydrologySettings settings, IntBinaryOperator natural) {
this.settings = settings; this.settings = settings;
this.natural = natural; this.natural = natural;
this.rasterizer = new ChunkRasterizer(settings); this.rasterizer = new ChunkRasterizer(settings, ChannelFluids.TIER);
} }
private Scene reach(Reach reach) { private Scene reach(Reach reach) {
@@ -862,7 +947,7 @@ public class ChunkWaterRasterTest {
private ChunkWaterRaster raster(int chunkX, int chunkZ) { private ChunkWaterRaster raster(int chunkX, int chunkZ) {
return rasters.computeIfAbsent(pack(chunkX, chunkZ), return rasters.computeIfAbsent(pack(chunkX, chunkZ),
key -> rasterizer.rasterize(chunkX, chunkZ, natural, windows(chunkX, chunkZ), regions(chunkX, chunkZ))); key -> rasterizer.rasterize(chunkX, chunkZ, natural::applyAsInt, windows(chunkX, chunkZ), regions(chunkX, chunkZ)));
} }
private List<IndexedWindow> windows(int chunkX, int chunkZ) { private List<IndexedWindow> windows(int chunkX, int chunkZ) {
@@ -15,6 +15,7 @@ import art.arcane.iris.generation.hydrology.plan.RegionKey;
import art.arcane.iris.generation.hydrology.plan.RegionPlan; import art.arcane.iris.generation.hydrology.plan.RegionPlan;
import art.arcane.iris.generation.hydrology.plan.WindowKey; import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.hydrology.plan.WindowPlan; import art.arcane.iris.generation.hydrology.plan.WindowPlan;
import art.arcane.iris.generation.hydrology.raster.ChannelFluids;
import art.arcane.iris.generation.hydrology.raster.ChunkRasterizer; import art.arcane.iris.generation.hydrology.raster.ChunkRasterizer;
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster; import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
import art.arcane.iris.generation.hydrology.raster.ReachIndex; import art.arcane.iris.generation.hydrology.raster.ReachIndex;
@@ -656,7 +657,7 @@ public class PondPlannerTest {
private static void assertRasterRimContained(HydrologySettings settings, Pond pond, DoubleBinaryOperator natural) { private static void assertRasterRimContained(HydrologySettings settings, Pond pond, DoubleBinaryOperator natural) {
Map<Long, ChunkWaterRaster> rasters = new HashMap<>(); Map<Long, ChunkWaterRaster> rasters = new HashMap<>();
ChunkRasterizer rasterizer = new ChunkRasterizer(settings); ChunkRasterizer rasterizer = new ChunkRasterizer(settings, ChannelFluids.TIER);
int raised = 0; int raised = 0;
int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int[] wet : wetNodes(pond.mask())) { for (int[] wet : wetNodes(pond.mask())) {
@@ -123,8 +123,8 @@ public class IrisEngineGenerationRuntimeScopeTest {
AtomicInteger workerNaturals = new AtomicInteger(); AtomicInteger workerNaturals = new AtomicInteger();
doAnswer(invocation -> { doAnswer(invocation -> {
observe(engine, historical, caller, naturals, workerNaturals, foreign); observe(engine, historical, caller, naturals, workerNaturals, foreign);
return (int) Math.round(bowl(invocation.getArgument(0), invocation.getArgument(1))); return bowl(invocation.getArgument(0), invocation.getArgument(1));
}).when(historicalComplex).naturalTrueHeight(anyInt(), anyInt()); }).when(historicalComplex).naturalHeight(anyInt(), anyInt());
IrisEngine.GenerationRuntimeBinding binding = detachedBinding(engine, historical.runtime); IrisEngine.GenerationRuntimeBinding binding = detachedBinding(engine, historical.runtime);
HydrologyRuntime hydrology; HydrologyRuntime hydrology;
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(binding)) { try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(binding)) {
@@ -84,7 +84,7 @@ public class SchemaBuilderIdentityTest {
@ClassRule @ClassRule
public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean(); public static final PlatformLeakGuard PLATFORM_GUARD = PlatformLeakGuard.clean();
private static final String EXPECTED_SCHEMA_DIGEST = "b8dd16d1a3188e8150a68a9d5b2964aed5094d679c0f941e3703aec675f87e20"; private static final String EXPECTED_SCHEMA_DIGEST = "2224e73a1e9d76231d4df8bb1992451340e5f533b3fbafae9f4e1437747e666c";
private static final List<Class<?>> SCHEMA_ROOTS = List.of( private static final List<Class<?>> SCHEMA_ROOTS = List.of(
IrisDimension.class, IrisDimension.class,
@@ -83,7 +83,7 @@ public final class FakeHydrology implements AutoCloseable {
}); });
HydrologySettings settings = settings(seaLevel); HydrologySettings settings = settings(seaLevel);
HydrologyPlanCache cache = new HydrologyPlanCache(settings, this::region, this::window, pool, mainThread); HydrologyPlanCache cache = new HydrologyPlanCache(settings, this::region, this::window, pool, mainThread);
this.runtime = new HydrologyRuntime(settings, cache, naturalHeight, mainThread, System::nanoTime); this.runtime = new HydrologyRuntime(settings, cache, naturalHeight::applyAsInt, mainThread, System::nanoTime);
} }
public static HydrologySettings settings(int seaLevel) { public static HydrologySettings settings(int seaLevel) {
@@ -9,6 +9,7 @@ import art.arcane.iris.generation.hydrology.plan.RegionKey;
import art.arcane.iris.generation.hydrology.plan.RegionPlan; import art.arcane.iris.generation.hydrology.plan.RegionPlan;
import art.arcane.iris.generation.hydrology.plan.WindowKey; import art.arcane.iris.generation.hydrology.plan.WindowKey;
import art.arcane.iris.generation.hydrology.plan.WindowPlan; import art.arcane.iris.generation.hydrology.plan.WindowPlan;
import art.arcane.iris.generation.hydrology.raster.ChannelFluids;
import art.arcane.iris.generation.hydrology.raster.ChunkRasterizer; import art.arcane.iris.generation.hydrology.raster.ChunkRasterizer;
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster; import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
import art.arcane.iris.generation.hydrology.raster.ReachIndex; import art.arcane.iris.generation.hydrology.raster.ReachIndex;
@@ -40,7 +41,7 @@ public final class WaterScene {
public WaterScene(IrisHydrology config, IntBinaryOperator natural) { public WaterScene(IrisHydrology config, IntBinaryOperator natural) {
this.natural = natural; this.natural = natural;
this.rasterizer = new ChunkRasterizer(HydrologySettings.compile(config, SEED, SEA_LEVEL, MIN_Y, MAX_Y)); this.rasterizer = new ChunkRasterizer(HydrologySettings.compile(config, SEED, SEA_LEVEL, MIN_Y, MAX_Y), ChannelFluids.TIER);
} }
public WaterScene reach(Reach reach) { public WaterScene reach(Reach reach) {
@@ -55,7 +56,7 @@ public final class WaterScene {
public ChunkWaterRaster raster(int chunkX, int chunkZ) { public ChunkWaterRaster raster(int chunkX, int chunkZ) {
return rasters.computeIfAbsent(((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL), return rasters.computeIfAbsent(((long) chunkX << 32) | (chunkZ & 0xFFFFFFFFL),
key -> rasterizer.rasterize(chunkX, chunkZ, natural, windows(chunkX, chunkZ), regions(chunkX, chunkZ))); key -> rasterizer.rasterize(chunkX, chunkZ, natural::applyAsInt, windows(chunkX, chunkZ), regions(chunkX, chunkZ)));
} }
public int terrain(int x, int z) { public int terrain(int x, int z) {
@@ -86,7 +86,7 @@ final class HydrologyBlockMetrics {
@Override @Override
public int natural(int x, int z) { 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; 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.WindowKey;
import art.arcane.iris.generation.hydrology.plan.WindowPlan; import art.arcane.iris.generation.hydrology.plan.WindowPlan;
import art.arcane.iris.generation.hydrology.plan.WindowPlanner; 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.ChunkRasterizer;
import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster; import art.arcane.iris.generation.hydrology.raster.ChunkWaterRaster;
import art.arcane.iris.generation.hydrology.raster.ReachIndex; import art.arcane.iris.generation.hydrology.raster.ReachIndex;
import art.arcane.iris.generation.hydrology.water.Pond; import art.arcane.iris.generation.hydrology.water.Pond;
import art.arcane.iris.generation.hydrology.raster.NaturalHeights;
import com.sun.management.OperatingSystemMXBean; import com.sun.management.OperatingSystemMXBean;
import java.lang.management.ManagementFactory; import java.lang.management.ManagementFactory;
@@ -32,7 +34,6 @@ import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicLong;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.IntBinaryOperator;
final class HydrologyPlanHarness { final class HydrologyPlanHarness {
private static final double TILE_AREA = 1024D * 1024D; private static final double TILE_AREA = 1024D * 1024D;
@@ -43,7 +44,7 @@ final class HydrologyPlanHarness {
static final class Plans { static final class Plans {
private final HydrologySettings settings; private final HydrologySettings settings;
private final IntBinaryOperator naturalHeight; private final NaturalHeights naturalHeight;
private final HydrologyWindows.WindowRange planned; private final HydrologyWindows.WindowRange planned;
private final CountingTerrain counting; private final CountingTerrain counting;
private final HydrologyPlanners planners; private final HydrologyPlanners planners;
@@ -54,13 +55,13 @@ final class HydrologyPlanHarness {
private final Map<WindowKey, Double> windowCpu = new HashMap<>(); private final Map<WindowKey, Double> windowCpu = new HashMap<>();
private final List<String> failures = new ArrayList<>(); 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.settings = settings;
this.naturalHeight = naturalHeight; this.naturalHeight = naturalHeight;
this.planned = range.grown(1); this.planned = range.grown(1);
this.counting = new CountingTerrain(terrain); this.counting = new CountingTerrain(terrain);
this.planners = HydrologyPlanners.create(settings, counting); 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); Set<RegionKey> regionKeys = new TreeSet<>(REGION_ORDER);
for (WindowKey key : planned.keys()) { for (WindowKey key : planned.keys()) {
regionKeys.addAll(WindowPlanner.lakeRegions(key)); 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.hydrology.raster.ChunkWaterRaster;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.hydrology.raster.NaturalHeights;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@@ -27,7 +28,6 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask; import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.Future; import java.util.concurrent.Future;
import java.util.function.IntBinaryOperator;
public final class HydrologyProbe { public final class HydrologyProbe {
private static final int RASTER_CACHE = 8192; 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) { ExecutorService workers, HydrologyVoxels.ChunkSource blocks, HydrologyUnderground.Source underground, int worldHeight) {
Inputs { Inputs {
Objects.requireNonNull(settings, "settings"); Objects.requireNonNull(settings, "settings");
@@ -98,7 +98,7 @@ public final class HydrologyProbe {
ForkJoinPool planningPool = MultiBurst.hydrology.submit((Callable<ForkJoinPool>) ForkJoinTask::getPool).get(); ForkJoinPool planningPool = MultiBurst.hydrology.submit((Callable<ForkJoinPool>) ForkJoinTask::getPool).get();
ExecutorService workers = Executors.newFixedThreadPool(configuration.threads()); ExecutorService workers = Executors.newFixedThreadPool(configuration.threads());
try { 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), (chunkX, chunkZ) -> HydrologyVoxels.generate(engine, chunkX, chunkZ), HydrologyUnderground.generated(engine, workers),
engine.getTarget().getHeight()); engine.getTarget().getHeight());
HydrologyReport.Result measured = measure(inputs, target.windows()); 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.Pond;
import art.arcane.iris.generation.hydrology.water.Reach; import art.arcane.iris.generation.hydrology.water.Reach;
import art.arcane.iris.generation.hydrology.water.ReachStyle; 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.Consumer;
import java.util.function.IntBinaryOperator;
final class HydrologyRasterMetrics { final class HydrologyRasterMetrics {
private final HydrologySettings settings; private final HydrologySettings settings;
private final HydrologyPlanMetrics plans; private final HydrologyPlanMetrics plans;
private final Rasters rasters; private final Rasters rasters;
private final IntBinaryOperator naturalHeight; private final NaturalHeights naturalHeight;
private final HydrologyWindows.WindowRange range; private final HydrologyWindows.WindowRange range;
private final Consumer<String> examples; private final Consumer<String> examples;
@@ -25,7 +25,7 @@ final class HydrologyRasterMetrics {
ChunkWaterRaster get(int chunkX, int chunkZ); 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) { HydrologyWindows.WindowRange range, Consumer<String> examples) {
this.settings = settings; this.settings = settings;
this.plans = plans; this.plans = plans;
@@ -115,7 +115,7 @@ final class HydrologyRasterMetrics {
continue; continue;
} }
int terrain = raster.terrainHeight(x & 15, z & 15); 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) { if (terrain > natural + maxFill) {
overCap++; overCap++;
examples.accept("berm raised " + x + "," + z + " from " + natural + " to " + terrain); examples.accept("berm raised " + x + "," + z + " from " + natural + " to " + terrain);
@@ -131,7 +131,7 @@ final class HydrologyRasterMetrics {
int terrain(int x, int z) { int terrain(int x, int z) {
ChunkWaterRaster raster = rasters.get(x >> 4, z >> 4); 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) { 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.hydrology.water.ReachStyle;
import art.arcane.iris.generation.runtime.Engine; import art.arcane.iris.generation.runtime.Engine;
import art.arcane.iris.generation.terrain.IrisDimension; import art.arcane.iris.generation.terrain.IrisDimension;
import art.arcane.iris.generation.hydrology.raster.NaturalHeights;
import javax.imageio.ImageIO; import javax.imageio.ImageIO;
import java.awt.BasicStroke; import java.awt.BasicStroke;
@@ -33,7 +34,6 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.function.IntBinaryOperator;
public final class RiverTransectProbe { public final class RiverTransectProbe {
static final int NO_WATER = Integer.MIN_VALUE; static final int NO_WATER = Integer.MIN_VALUE;
@@ -116,16 +116,16 @@ public final class RiverTransectProbe {
engine.getHeight()); engine.getHeight());
IrisHydrologyTerrainSource terrain = new IrisHydrologyTerrainSource(engine.getComplex(), dimension, settings, MultiBurst.hydrology, IrisHydrologyTerrainSource terrain = new IrisHydrologyTerrainSource(engine.getComplex(), dimension, settings, MultiBurst.hydrology,
MultiBurst.hydrology.parallelism(), HydrologyBinding.UNBOUND); 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<>(); List<Rendered> rendered = new ArrayList<>();
for (WindowKey key : target.windows().keys()) { 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; 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 { File output) throws Exception {
Map<Long, ChunkWaterRaster> rasters = new HashMap<>(); Map<Long, ChunkWaterRaster> rasters = new HashMap<>();
Columns columns = (x, z) -> { Columns columns = (x, z) -> {
@@ -134,7 +134,7 @@ public final class RiverTransectProbe {
} }
ChunkWaterRaster raster = rasters.computeIfAbsent(HydrologyVoxels.chunkKey(x >> 4, z >> 4), ChunkWaterRaster raster = rasters.computeIfAbsent(HydrologyVoxels.chunkKey(x >> 4, z >> 4),
ignored -> plans.rasterize(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), 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)); raster.role(x & 15, z & 15));
}; };