mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 08:50:06 +00:00
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:
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.")
|
||||
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.")
|
||||
private IrisHydrologyDrainage drainage = new IrisHydrologyDrainage();
|
||||
|
||||
|
||||
+37
@@ -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;
|
||||
}
|
||||
+2
-2
@@ -24,12 +24,12 @@ public class IrisHydrologyDeformation {
|
||||
|
||||
@MinNumber(0)
|
||||
@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;
|
||||
|
||||
@MinNumber(0)
|
||||
@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;
|
||||
|
||||
@Description("Keeps caves from opening under water, so rivers and ponds never drain into them.")
|
||||
|
||||
+4
-1
@@ -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.")
|
||||
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.")
|
||||
private IrisHydrologyPermission streams = null;
|
||||
|
||||
@Description("Whether ponds may form in this area. With no value set anywhere they are allowed.")
|
||||
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;
|
||||
|
||||
@RegistryListResource(IrisBiome.class)
|
||||
|
||||
+9
-8
@@ -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.HydrologyFeatureRef;
|
||||
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.ChunkWaterRaster;
|
||||
import art.arcane.iris.generation.hydrology.raster.NaturalHeights;
|
||||
import art.arcane.iris.generation.hydrology.underground.SeaInlet;
|
||||
import art.arcane.iris.generation.hydrology.underground.SeaInletRasterizer;
|
||||
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.function.BooleanSupplier;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
import java.util.function.LongSupplier;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
@@ -68,7 +69,7 @@ public final class HydrologyRuntime {
|
||||
private final HydrologySettings settings;
|
||||
private final HydrologyPlanCache cache;
|
||||
private final ChunkRasterizer rasterizer;
|
||||
private final IntBinaryOperator naturalHeight;
|
||||
private final NaturalHeights naturalHeight;
|
||||
private final BooleanSupplier mainThread;
|
||||
private final RasterLru rasters;
|
||||
private final ThreadLocal<LastRaster> last;
|
||||
@@ -109,17 +110,17 @@ public final class HydrologyRuntime {
|
||||
this.caveFluidPolicies = false;
|
||||
}
|
||||
|
||||
public HydrologyRuntime(HydrologySettings settings, HydrologyPlanCache cache, IntBinaryOperator naturalHeight,
|
||||
public HydrologyRuntime(HydrologySettings settings, HydrologyPlanCache cache, NaturalHeights naturalHeight,
|
||||
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,
|
||||
BooleanSupplier mainThread, LongSupplier ticker, HydrologyBinding binding,
|
||||
private HydrologyRuntime(HydrologySettings settings, HydrologyPlanCache cache, NaturalHeights naturalHeight,
|
||||
ChannelFluids channelFluids, BooleanSupplier mainThread, LongSupplier ticker, HydrologyBinding binding,
|
||||
Function<PreparedHydrologyCacheIdentity, HydrologyPlanStore> stores, boolean caveFluidPolicies) {
|
||||
this.settings = Objects.requireNonNull(settings, "settings");
|
||||
this.cache = Objects.requireNonNull(cache, "cache");
|
||||
this.rasterizer = new ChunkRasterizer(settings);
|
||||
this.rasterizer = new ChunkRasterizer(settings, channelFluids);
|
||||
this.naturalHeight = Objects.requireNonNull(naturalHeight, "naturalHeight");
|
||||
this.mainThread = Objects.requireNonNull(mainThread, "mainThread");
|
||||
this.rasters = new RasterLru();
|
||||
@@ -160,7 +161,7 @@ public final class HydrologyRuntime {
|
||||
HydrologyPlanCache cache = HydrologyPlanCache.create(settings, planners.regions(), planners.windows(), MultiBurst.hydrology, mainThread);
|
||||
String dimensionKey = dimension.getLoadKey();
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
+25
-7
@@ -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.HydrologyTerrainSource;
|
||||
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.terrain.InferredType;
|
||||
import art.arcane.iris.generation.terrain.IrisDimension;
|
||||
import art.arcane.iris.generation.terrain.IrisRegion;
|
||||
import art.arcane.iris.studio.generation.BiomeBuffetLayout;
|
||||
import art.arcane.volmlib.util.noise.CNG;
|
||||
import art.arcane.volmlib.util.stream.ProceduralStream;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -25,9 +27,10 @@ import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
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 POLICY_CAPACITY = 4096;
|
||||
private static final long SALT_CLIMATE = 0x434c494d415445L;
|
||||
|
||||
private final IrisComplex complex;
|
||||
private final IrisDimension dimension;
|
||||
@@ -40,6 +43,8 @@ public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource
|
||||
private final BiomeBuffetLayout buffet;
|
||||
private final IrisBiome focus;
|
||||
private final HydrologyBinding binding;
|
||||
private final HydrologySettings.Climate climate;
|
||||
private final CNG wetness;
|
||||
private final ConcurrentHashMap<PolicyKey, Policy> policies = new ConcurrentHashMap<>();
|
||||
|
||||
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.focus = complex.getFocusBiome();
|
||||
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
|
||||
@@ -73,8 +80,13 @@ public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource
|
||||
}
|
||||
}
|
||||
|
||||
public int naturalHeight(int x, int z) {
|
||||
return complex.naturalTrueHeight(x, z);
|
||||
public double naturalSurface(int x, int 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
|
||||
@@ -150,12 +162,17 @@ public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource
|
||||
Policy policy = policy(biomes.get(x, z), regions.get(x, z));
|
||||
out.height()[index] = (float) height;
|
||||
out.ocean()[index] = StrictMath.round(height) < settings.seaLevel() && oceanIntent(x, z);
|
||||
out.rainfall()[index] = policy.rainfall();
|
||||
out.pondDensity()[index] = policy.pondDensity();
|
||||
float wet = policy.climateZones() ? wetness(x, z) : 1f;
|
||||
out.rainfall()[index] = policy.rainfall() * wet;
|
||||
out.pondDensity()[index] = policy.pondDensity() * wet;
|
||||
out.permission()[index] = policy.permission();
|
||||
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) {
|
||||
BiomeBuffetLayout.Cell cell = buffet == null ? null : buffet.terrain(x, z);
|
||||
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 ponds = inherited(biome, region, IrisHydrologyPolicy::getPonds, IrisHydrologyPermission.ALLOW);
|
||||
String fluid = inherited(biome, region, IrisHydrologyPolicy::getFluid, null);
|
||||
boolean climateZones = inherited(biome, region, IrisHydrologyPolicy::getClimateZones, Boolean.TRUE);
|
||||
requireMultiplier("rainfall", rainfall, biome, region);
|
||||
requireMultiplier("pondDensity", pondDensity, biome, region);
|
||||
if (fluid != null && !settings.fluids().containsKey(fluid)) {
|
||||
throw new IllegalArgumentException("Hydrology policy for " + where(biome, region) + " names fluid '" + fluid
|
||||
+ "', 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) {
|
||||
@@ -214,7 +232,7 @@ public final class IrisHydrologyTerrainSource implements HydrologyTerrainSource
|
||||
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) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import art.arcane.iris.generation.decoration.IrisDecorationPart;
|
||||
import art.arcane.iris.generation.decoration.IrisDecorator;
|
||||
import art.arcane.iris.generation.hydrology.config.IrisCavePools;
|
||||
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.IrisHydrologyDeformation;
|
||||
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.underground.SeaInletRasterizer;
|
||||
import art.arcane.iris.generation.hydrology.water.HydrologyTier;
|
||||
import art.arcane.iris.generation.noise.IrisGeneratorStyle;
|
||||
import art.arcane.iris.generation.terrain.IrisMaterialPalette;
|
||||
import com.google.gson.Gson;
|
||||
|
||||
@@ -51,6 +53,7 @@ public final class HydrologySettings {
|
||||
private final int maxY;
|
||||
private final boolean enabled;
|
||||
private final float density;
|
||||
private final Climate climate;
|
||||
private final Drainage drainage;
|
||||
private final List<HydrologyTier> tiers;
|
||||
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());
|
||||
}
|
||||
this.density = (float) config.getDensity();
|
||||
this.climate = climate(config.getClimate());
|
||||
this.drainage = drainage(config.getDrainage());
|
||||
this.tiers = tiers(config.getTiers());
|
||||
this.ponds = ponds(config.getPonds());
|
||||
@@ -135,6 +139,10 @@ public final class HydrologySettings {
|
||||
return density;
|
||||
}
|
||||
|
||||
public Climate climate() {
|
||||
return climate;
|
||||
}
|
||||
|
||||
public Drainage drainage() {
|
||||
return drainage;
|
||||
}
|
||||
@@ -167,6 +175,22 @@ public final class HydrologySettings {
|
||||
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) {
|
||||
return new Drainage((float) config.getFillEpsilon(), config.getBreachDepth(), config.isWetnessThinning(),
|
||||
(float) config.getMinimumHeadKeep(), (float) config.getHeadRelief(), (float) config.getSaturationCatchment());
|
||||
@@ -468,6 +492,7 @@ public final class HydrologySettings {
|
||||
private long computeFingerprint() {
|
||||
Fingerprint hash = new Fingerprint().add(seed).add(seaLevel).add(minY).add(maxY).add(enabled)
|
||||
.add(density).add(prefetchRings).add(persist);
|
||||
addClimate(hash);
|
||||
addDrainage(hash);
|
||||
addTiers(hash);
|
||||
addPonds(hash);
|
||||
@@ -477,6 +502,12 @@ public final class HydrologySettings {
|
||||
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) {
|
||||
hash.add(drainage.fillEpsilon()).add(drainage.breachDepth()).add(drainage.wetnessThinning())
|
||||
.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 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,
|
||||
float saturationCatchment) {
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ final class BedProfile {
|
||||
private static final long SALT_BROAD = 0x42454442524f4144L;
|
||||
private static final long SALT_DETAIL = 0x4245444445544149L;
|
||||
private static final long SALT_TEXTURE = 0x4245445445585455L;
|
||||
private static final long SALT_ROUGHNESS = 0x42414e4b524f5547L;
|
||||
|
||||
private BedProfile() {
|
||||
}
|
||||
@@ -18,6 +19,10 @@ final class BedProfile {
|
||||
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) {
|
||||
double bounded = Math.clamp(u, 0D, 1D);
|
||||
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.Objects;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
public final class ChunkRasterizer {
|
||||
private static final int CHUNK = 16;
|
||||
private static final int CANDIDATE_HALO = 48;
|
||||
|
||||
private final HydrologySettings settings;
|
||||
private final ChannelFluids channelFluids;
|
||||
private final int halo;
|
||||
|
||||
public ChunkRasterizer(HydrologySettings settings) {
|
||||
public ChunkRasterizer(HydrologySettings settings, ChannelFluids channelFluids) {
|
||||
this.settings = Objects.requireNonNull(settings, "settings");
|
||||
this.channelFluids = Objects.requireNonNull(channelFluids, "channelFluids");
|
||||
float widest = 0f;
|
||||
for (HydrologyTier tier : settings.tiers()) {
|
||||
widest = Math.max(widest, tier.maxHalfWidth());
|
||||
@@ -33,13 +34,13 @@ public final class ChunkRasterizer {
|
||||
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) {
|
||||
Objects.requireNonNull(naturalHeight, "naturalHeight");
|
||||
ChunkFeatures features = ChunkFeatures.gather(settings, chunkX, chunkZ, halo, windows, lakeRegionPlans, lakeRegions(chunkX, chunkZ));
|
||||
if (features.inert()) {
|
||||
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);
|
||||
}
|
||||
+53
-16
@@ -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.cave.HydrologyCaveCell;
|
||||
import art.arcane.iris.generation.hydrology.grid.CellHash;
|
||||
import art.arcane.iris.generation.hydrology.plan.HydrologySettings;
|
||||
import art.arcane.iris.generation.hydrology.underground.SeaInlet;
|
||||
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 java.util.Arrays;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
final class RasterComposer {
|
||||
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 CONE_EXPONENT = 0.8D;
|
||||
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 ChunkFeatures features;
|
||||
private final ChannelFluids channelFluids;
|
||||
private final int originX;
|
||||
private final int originZ;
|
||||
private final double[] exact = new double[CELLS];
|
||||
private final int[] natural = new int[CELLS];
|
||||
private final int[] terrain = 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 ReachCursor cursor = new ReachCursor();
|
||||
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.features = features;
|
||||
this.channelFluids = channelFluids;
|
||||
this.originX = chunkX * CHUNK - 1;
|
||||
this.originZ = chunkZ * CHUNK - 1;
|
||||
Arrays.fill(head, Integer.MIN_VALUE);
|
||||
@@ -54,11 +56,12 @@ final class RasterComposer {
|
||||
Arrays.fill(distance, Double.POSITIVE_INFINITY);
|
||||
}
|
||||
|
||||
ChunkWaterRaster compose(IntBinaryOperator naturalHeight) {
|
||||
ChunkWaterRaster compose(NaturalHeights naturalHeight) {
|
||||
this.naturalHeight = naturalHeight;
|
||||
for (int cell = 0; cell < CELLS; 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];
|
||||
}
|
||||
}
|
||||
@@ -107,7 +110,7 @@ final class RasterComposer {
|
||||
return;
|
||||
}
|
||||
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)) {
|
||||
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));
|
||||
}
|
||||
|
||||
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;
|
||||
Reach reach = hit.reach;
|
||||
int point = hit.point();
|
||||
String area = channelFluids.channelFluid(x, z);
|
||||
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]);
|
||||
feature[cell] = reach.id();
|
||||
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) {
|
||||
@@ -162,7 +166,8 @@ final class RasterComposer {
|
||||
u = BedProfile.position(u, -variation);
|
||||
}
|
||||
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 {
|
||||
raise = BankProfile.levee(bankTier.bankStyle(), surface, ground, u, noise(x, z), settings.deformation().maxFill());
|
||||
}
|
||||
@@ -205,15 +210,47 @@ final class RasterComposer {
|
||||
distance[cell] = best;
|
||||
}
|
||||
|
||||
private double coneSlope(int cell) {
|
||||
double slope = Math.max(Math.abs(natural[cell + 1] - natural[cell - 1]), Math.abs(natural[cell + GRID] - natural[cell - GRID])) / 2D;
|
||||
private double coneSlope(double slope) {
|
||||
HydrologySettings.Deformation deformation = settings.deformation();
|
||||
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) {
|
||||
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) {
|
||||
@@ -349,7 +386,7 @@ final class RasterComposer {
|
||||
if (gx >= 0 && gz >= 0 && gx < GRID && gz < GRID && !corner(gz * GRID + gx)) {
|
||||
return natural[gz * GRID + gx];
|
||||
}
|
||||
return naturalHeight.applyAsInt(x, z);
|
||||
return (int) Math.round(naturalHeight.height(x, z));
|
||||
}
|
||||
|
||||
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. */
|
||||
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. */
|
||||
|
||||
+46
@@ -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) {
|
||||
IrisRegion beta = fixture.data.getRegionLoader().load("beta");
|
||||
beta.setHydrologyPolicy(new IrisHydrologyPolicy().setFluid("slime"));
|
||||
|
||||
+49
@@ -48,6 +48,55 @@ public class HydrologySettingsTest {
|
||||
@Rule
|
||||
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
|
||||
public void tierForPicksHighestTierBelowCatchment() {
|
||||
HydrologySettings settings = compile(new IrisHydrology());
|
||||
|
||||
+94
-9
@@ -261,7 +261,7 @@ public class ChunkWaterRasterTest {
|
||||
assertTrue("the chain ends at or below the first end grade", previous <= 60);
|
||||
List<IndexedWindow> reversed = new ArrayList<>(scene.windows(31, 34));
|
||||
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 lx = 0; lx < 16; lx++) {
|
||||
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);
|
||||
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()));
|
||||
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()));
|
||||
int[] column = new int[2];
|
||||
int open = 0;
|
||||
@@ -440,8 +440,8 @@ public class ChunkWaterRasterTest {
|
||||
samples.incrementAndGet();
|
||||
return 70;
|
||||
};
|
||||
ChunkRasterizer rasterizer = new ChunkRasterizer(settings);
|
||||
ChunkWaterRaster raster = rasterizer.rasterize(0, 0, natural, List.of(own), regions(0, 0, List.of()));
|
||||
ChunkRasterizer rasterizer = new ChunkRasterizer(settings, ChannelFluids.TIER);
|
||||
ChunkWaterRaster raster = rasterizer.rasterize(0, 0, natural::applyAsInt, List.of(own), regions(0, 0, List.of()));
|
||||
assertTrue(raster.empty());
|
||||
assertFalse(raster.provisional());
|
||||
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));
|
||||
assertTrue(ChunkWaterRaster.PROVISIONAL.provisional());
|
||||
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",
|
||||
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
|
||||
@@ -525,7 +610,7 @@ public class ChunkWaterRasterTest {
|
||||
ChunkWaterRaster forward = scene.raster(31, 12);
|
||||
List<IndexedWindow> reversed = new ArrayList<>(scene.windows(31, 12));
|
||||
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 lx = 0; lx < 16; lx++) {
|
||||
assertEquals(forward.waterHead(lx, lz), backward.waterHead(lx, lz));
|
||||
@@ -837,7 +922,7 @@ public class ChunkWaterRasterTest {
|
||||
private Scene(HydrologySettings settings, IntBinaryOperator natural) {
|
||||
this.settings = settings;
|
||||
this.natural = natural;
|
||||
this.rasterizer = new ChunkRasterizer(settings);
|
||||
this.rasterizer = new ChunkRasterizer(settings, ChannelFluids.TIER);
|
||||
}
|
||||
|
||||
private Scene reach(Reach reach) {
|
||||
@@ -862,7 +947,7 @@ public class ChunkWaterRasterTest {
|
||||
|
||||
private ChunkWaterRaster raster(int chunkX, int 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) {
|
||||
|
||||
@@ -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.WindowKey;
|
||||
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.ChunkWaterRaster;
|
||||
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) {
|
||||
Map<Long, ChunkWaterRaster> rasters = new HashMap<>();
|
||||
ChunkRasterizer rasterizer = new ChunkRasterizer(settings);
|
||||
ChunkRasterizer rasterizer = new ChunkRasterizer(settings, ChannelFluids.TIER);
|
||||
int raised = 0;
|
||||
int[][] sides = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||
for (int[] wet : wetNodes(pond.mask())) {
|
||||
|
||||
+2
-2
@@ -123,8 +123,8 @@ public class IrisEngineGenerationRuntimeScopeTest {
|
||||
AtomicInteger workerNaturals = new AtomicInteger();
|
||||
doAnswer(invocation -> {
|
||||
observe(engine, historical, caller, naturals, workerNaturals, foreign);
|
||||
return (int) Math.round(bowl(invocation.getArgument(0), invocation.getArgument(1)));
|
||||
}).when(historicalComplex).naturalTrueHeight(anyInt(), anyInt());
|
||||
return bowl(invocation.getArgument(0), invocation.getArgument(1));
|
||||
}).when(historicalComplex).naturalHeight(anyInt(), anyInt());
|
||||
IrisEngine.GenerationRuntimeBinding binding = detachedBinding(engine, historical.runtime);
|
||||
HydrologyRuntime hydrology;
|
||||
try (IrisEngine.GenerationRuntimeScope ignored = engine.openGenerationRuntimeScope(binding)) {
|
||||
|
||||
@@ -84,7 +84,7 @@ public class SchemaBuilderIdentityTest {
|
||||
@ClassRule
|
||||
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(
|
||||
IrisDimension.class,
|
||||
|
||||
@@ -83,7 +83,7 @@ public final class FakeHydrology implements AutoCloseable {
|
||||
});
|
||||
HydrologySettings settings = settings(seaLevel);
|
||||
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) {
|
||||
|
||||
@@ -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.WindowKey;
|
||||
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.ChunkWaterRaster;
|
||||
import art.arcane.iris.generation.hydrology.raster.ReachIndex;
|
||||
@@ -40,7 +41,7 @@ public final class WaterScene {
|
||||
|
||||
public WaterScene(IrisHydrology config, IntBinaryOperator 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) {
|
||||
@@ -55,7 +56,7 @@ public final class WaterScene {
|
||||
|
||||
public ChunkWaterRaster raster(int chunkX, int chunkZ) {
|
||||
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) {
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user