mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-22 16:17:44 +00:00
Spring pools at headwaters and ring-ordered feature search
- channel.springWidthRatio / springLength open every surface river in a pool that shrinks where the ground falls away - Banks settings keep structural invariants only; authoring bounds stay in the validator - Feature searches walk tiles outward and stop early; find and goto report progress per ring
This commit is contained in:
1 parent
896e58e212
commit
d1c73025a0
12 files changed
+152
-56
No files matched your search
@@ -365,6 +365,8 @@ final class PackHydrologyValidator {
|
||||
validateRange(path, channel, "depth", 1D, 64D, 2D, 4D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "inset", 0, 3, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "maximumIncision", 1, 32, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, channel, "springWidthRatio", 1D, 4D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "springLength", 4, 96, errors);
|
||||
PackJsonFieldChecks.validateOptionalDoubleRange(path, channel, "roughness", 0D, 1D, errors);
|
||||
PackJsonFieldChecks.validateOptionalIntegerRange(path, channel, "roughnessWavelength", 4, 64, errors);
|
||||
}
|
||||
|
||||
@@ -251,23 +251,28 @@ public record HydrologyPlannerSettings(
|
||||
int cascadeRun,
|
||||
int waterfallMinimumDrop,
|
||||
double mouthFlareRatio,
|
||||
double springWidthRatio,
|
||||
int springLength,
|
||||
boolean exposeCutStrata
|
||||
) {
|
||||
// Structural invariants only; authoring bounds live in the pack validator.
|
||||
public Banks {
|
||||
if (inset < 1 || inset > 16 || freeboard < 1 || freeboard > 8
|
||||
|| !Double.isFinite(blendSlope) || blendSlope < 1D || blendSlope > 16D
|
||||
|| minimumBlendWidth < 1 || maximumBlendWidth < minimumBlendWidth || maximumBlendWidth > 128
|
||||
if (inset < 0 || freeboard < 0
|
||||
|| !Double.isFinite(blendSlope) || blendSlope <= 0D
|
||||
|| minimumBlendWidth < 1 || maximumBlendWidth < minimumBlendWidth
|
||||
|| !Double.isFinite(roughness) || roughness < 0D || roughness > 1D
|
||||
|| roughnessWavelength < 3 || roughnessWavelength > 128
|
||||
|| cascadeRun < 1 || cascadeRun > 16
|
||||
|| waterfallMinimumDrop < 2 || waterfallMinimumDrop > 24
|
||||
|| !Double.isFinite(mouthFlareRatio) || mouthFlareRatio < 1D || mouthFlareRatio > 4D) {
|
||||
|| roughnessWavelength < 1
|
||||
|| cascadeRun < 1
|
||||
|| waterfallMinimumDrop < 1
|
||||
|| !Double.isFinite(mouthFlareRatio) || mouthFlareRatio < 1D
|
||||
|| !Double.isFinite(springWidthRatio) || springWidthRatio < 1D
|
||||
|| springLength < 1) {
|
||||
throw new IllegalArgumentException("Surface bank settings are invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
public static Banks defaults() {
|
||||
return new Banks(1, 1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, true);
|
||||
return new Banks(1, 1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+42
-16
@@ -57,6 +57,7 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
private static final int MAXIMUM_CACHE_TILES = 64;
|
||||
@@ -286,40 +287,63 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
int x,
|
||||
int z,
|
||||
int maximumDistance
|
||||
) {
|
||||
return nearestFeature(types, profileKey, x, z, maximumDistance, (int visited) -> { });
|
||||
}
|
||||
|
||||
/**
|
||||
* Walks tiles outward from the origin, planning each on demand, and stops as soon as no unvisited
|
||||
* ring can hold a nearer feature. {@code progress} receives the running tile count after every ring.
|
||||
*/
|
||||
public Optional<HydrologyFeatureRef> nearestFeature(
|
||||
Set<HydrologyFeatureType> types,
|
||||
String profileKey,
|
||||
int x,
|
||||
int z,
|
||||
int maximumDistance,
|
||||
IntConsumer progress
|
||||
) {
|
||||
Objects.requireNonNull(types);
|
||||
Objects.requireNonNull(progress);
|
||||
if (types.isEmpty() || maximumDistance < 0) {
|
||||
return Optional.empty();
|
||||
}
|
||||
int tileSize = settings.routing().tileSize();
|
||||
int publicationRadius = settings.publicationRadius();
|
||||
int minimumTileX = tileCoordinate((long) x - maximumDistance - publicationRadius, tileSize);
|
||||
int maximumTileX = tileCoordinate((long) x + maximumDistance + publicationRadius, tileSize);
|
||||
int minimumTileZ = tileCoordinate((long) z - maximumDistance - publicationRadius, tileSize);
|
||||
int maximumTileZ = tileCoordinate((long) z + maximumDistance + publicationRadius, tileSize);
|
||||
long tileCount = HydrologyFeatureSearchBounds.tileCount(
|
||||
x, z, maximumDistance, tileSize, publicationRadius);
|
||||
if (tileCount > MAXIMUM_FEATURE_SEARCH_TILES) {
|
||||
throw new IllegalArgumentException("Hydrology feature search exceeds the bounded tile limit.");
|
||||
}
|
||||
int originTileX = tileCoordinate(x, tileSize);
|
||||
int originTileZ = tileCoordinate(z, tileSize);
|
||||
int ringLimit = HydrologyFeatureSearch.ringLimit(maximumDistance, tileSize, publicationRadius);
|
||||
HydrologyFeatureRef nearest = null;
|
||||
long nearestDistanceSquared = Long.MAX_VALUE;
|
||||
for (int tileZ = minimumTileZ; tileZ <= maximumTileZ; tileZ++) {
|
||||
for (int tileX = minimumTileX; tileX <= maximumTileX; tileX++) {
|
||||
HydrologyTile tile = cache.get(new HydrologyTileKey(tileX, tileZ));
|
||||
int visited = 0;
|
||||
for (int ring = 0; ring <= ringLimit; ring++) {
|
||||
long lowerBound = HydrologyFeatureSearch.lowerBound(ring, tileSize, publicationRadius);
|
||||
if (nearest != null && lowerBound * lowerBound > nearestDistanceSquared) {
|
||||
break;
|
||||
}
|
||||
for (HydrologyTileKey key : HydrologyFeatureSearch.ring(originTileX, originTileZ, ring)) {
|
||||
HydrologyTile tile = cache.get(key);
|
||||
visited++;
|
||||
HydrologyFeatureRef feature = tile.nearestFeature(
|
||||
types, profileKey, x, z, maximumDistance).orElse(null);
|
||||
if (feature != null) {
|
||||
long deltaX = (long) feature.x() - x;
|
||||
long deltaZ = (long) feature.z() - z;
|
||||
long distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
|
||||
if (nearest == null || distanceSquared < nearestDistanceSquared
|
||||
|| distanceSquared == nearestDistanceSquared && feature.id() < nearest.id()) {
|
||||
nearest = feature;
|
||||
nearestDistanceSquared = distanceSquared;
|
||||
}
|
||||
if (feature == null) {
|
||||
continue;
|
||||
}
|
||||
long deltaX = (long) feature.x() - x;
|
||||
long deltaZ = (long) feature.z() - z;
|
||||
long distanceSquared = deltaX * deltaX + deltaZ * deltaZ;
|
||||
if (nearest == null || distanceSquared < nearestDistanceSquared
|
||||
|| distanceSquared == nearestDistanceSquared && feature.id() < nearest.id()) {
|
||||
nearest = feature;
|
||||
nearestDistanceSquared = distanceSquared;
|
||||
}
|
||||
}
|
||||
progress.accept(visited);
|
||||
}
|
||||
return Optional.ofNullable(nearest);
|
||||
}
|
||||
@@ -677,6 +701,8 @@ public final class IrisHydrologyRuntime implements AutoCloseable {
|
||||
flow.getCascadeRun(),
|
||||
flow.getWaterfallMinimumDrop(),
|
||||
surface.getMouths().getFlareRatio(),
|
||||
channel.getSpringWidthRatio(),
|
||||
channel.getSpringLength(),
|
||||
banks.isExposeCutStrata()
|
||||
)
|
||||
);
|
||||
|
||||
+33
-6
@@ -9,7 +9,6 @@ import java.util.Objects;
|
||||
|
||||
public final class ChannelProfileBuilder {
|
||||
private static final int SMOOTHING_RADIUS = 16;
|
||||
private static final int HEADWATER_TAPER = 48;
|
||||
private static final int MOUTH_FLARE = 32;
|
||||
|
||||
private final HydrologyPlannerSettings.Surface surface;
|
||||
@@ -65,11 +64,15 @@ public final class ChannelProfileBuilder {
|
||||
}
|
||||
double[] smoothWidth = smooth(width);
|
||||
double[] smoothDepth = smooth(depth);
|
||||
int taper = Math.min(HEADWATER_TAPER, count / 2);
|
||||
for (int station = 0; station < taper; station++) {
|
||||
double progress = SurfaceNoise.smoothStep(station / (double) taper);
|
||||
smoothWidth[station] = 1D + (smoothWidth[station] - 1D) * progress;
|
||||
smoothDepth[station] = 1D + (smoothDepth[station] - 1D) * progress;
|
||||
// The headwater opens as a spring pool: wider and one block deeper, narrowing to the cruise width.
|
||||
// Ground that falls away across the pool shrinks it so it never demands a cut the valley solver rejects.
|
||||
int spring = Math.min(surface.banks().springLength(), count / 2);
|
||||
double springRatio = surface.banks().springWidthRatio();
|
||||
for (int station = 0; station < spring; station++) {
|
||||
double remaining = 1D - SurfaceNoise.smoothStep(station / (double) spring);
|
||||
double localRatio = 1D + (springRatio - 1D) * springRoom(centerline, station, smoothWidth[station] * springRatio / 2D);
|
||||
smoothWidth[station] *= 1D + (localRatio - 1D) * remaining;
|
||||
smoothDepth[station] += remaining;
|
||||
}
|
||||
if (directOcean) {
|
||||
int flare = Math.min(MOUTH_FLARE, count / 3);
|
||||
@@ -87,6 +90,30 @@ public final class ChannelProfileBuilder {
|
||||
return new ChannelProfile(halfWidth, smoothDepth, bank);
|
||||
}
|
||||
|
||||
/** Fraction of the spring pool the ground allows: 1 on level ground, 0 where the pool ring would sit half the permitted cut lower. */
|
||||
private double springRoom(SurfaceCenterline centerline, int station, double reach) {
|
||||
int centerX = centerline.x()[station];
|
||||
int centerZ = centerline.z()[station];
|
||||
HydrologyTerrainSample center = sampler.sample(centerX, centerZ);
|
||||
if (center == null) {
|
||||
return 1D;
|
||||
}
|
||||
double normalX = centerline.normalX(station);
|
||||
double normalZ = centerline.normalZ(station);
|
||||
int lowest = center.naturalHeight();
|
||||
for (double direction = -1D; direction <= 1D; direction += 2D) {
|
||||
HydrologyTerrainSample side = sampler.sample(
|
||||
(int) StrictMath.round(centerX + normalX * reach * direction),
|
||||
(int) StrictMath.round(centerZ + normalZ * reach * direction));
|
||||
if (side != null) {
|
||||
lowest = Math.min(lowest, side.naturalHeight());
|
||||
}
|
||||
}
|
||||
double drop = center.naturalHeight() - lowest;
|
||||
double allowance = Math.max(1D, surface.maximumIncision() / 2D);
|
||||
return Math.max(0D, Math.min(1D, 1D - drop / allowance));
|
||||
}
|
||||
|
||||
static double[] smooth(double[] values) {
|
||||
int count = values.length;
|
||||
double[] smoothed = new double[count];
|
||||
|
||||
@@ -38,6 +38,16 @@ public class IrisSurfaceRiverChannelConfig {
|
||||
@Desc("Wavelength in blocks of the outline and bed variation.")
|
||||
private int roughnessWavelength = 16;
|
||||
|
||||
@MinNumber(1)
|
||||
@MaxNumber(4)
|
||||
@Desc("Width of the spring pool at the headwater relative to the channel width; 1 starts the river at its normal width.")
|
||||
private double springWidthRatio = 2.5D;
|
||||
|
||||
@MinNumber(4)
|
||||
@MaxNumber(96)
|
||||
@Desc("Blocks over which the spring pool narrows back to the channel width.")
|
||||
private int springLength = 24;
|
||||
|
||||
private static IrisStyledRange range(double min, double max, double zoom) {
|
||||
IrisGeneratorStyle style = new IrisGeneratorStyle(NoiseStyle.IRIS).zoomed(zoom);
|
||||
return new IrisStyledRange(min, max, style);
|
||||
|
||||
@@ -288,7 +288,9 @@ public class PackHydrologyValidatorTest {
|
||||
"inset": 4,
|
||||
"maximumIncision": 33,
|
||||
"roughness": 1.5,
|
||||
"roughnessWavelength": 65
|
||||
"roughnessWavelength": 65,
|
||||
"springWidthRatio": 5,
|
||||
"springLength": 97
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,7 +305,9 @@ public class PackHydrologyValidatorTest {
|
||||
"inset": -1,
|
||||
"maximumIncision": 0,
|
||||
"roughness": -0.1,
|
||||
"roughnessWavelength": 3
|
||||
"roughnessWavelength": 3,
|
||||
"springWidthRatio": 0.5,
|
||||
"springLength": 3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -318,6 +322,10 @@ public class PackHydrologyValidatorTest {
|
||||
assertContains(aboveResult.errors(), "maximumIncision must be at most 32");
|
||||
assertContains(aboveResult.errors(), "roughness must be at most 1.0");
|
||||
assertContains(aboveResult.errors(), "roughnessWavelength must be at most 64");
|
||||
assertContains(aboveResult.errors(), "springWidthRatio must be at most 4.0");
|
||||
assertContains(aboveResult.errors(), "springLength must be at most 96");
|
||||
assertContains(belowResult.errors(), "springWidthRatio must be at least 1.0");
|
||||
assertContains(belowResult.errors(), "springLength must be at least 4");
|
||||
assertContains(belowResult.errors(), "width.min must be at least 1.0");
|
||||
assertContains(belowResult.errors(), "depth.min must be at least 1.0");
|
||||
assertContains(belowResult.errors(), "inset must be at least 0");
|
||||
|
||||
+7
-14
@@ -144,20 +144,13 @@ public class HydrologyPlannerSettingsTest {
|
||||
assertEquals(32, banks.maximumBlendWidth());
|
||||
assertEquals(6, banks.waterfallMinimumDrop());
|
||||
assertTrue(banks.exposeCutStrata());
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(
|
||||
0, 1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(
|
||||
1, 0, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(
|
||||
1, 1, 0.5D, 4, 32, 0.25D, 16, 2, 6, 1.6D, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(
|
||||
1, 1, 3D, 40, 32, 0.25D, 16, 2, 6, 1.6D, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(
|
||||
1, 1, 3D, 4, 32, 0.25D, 16, 2, 1, 1.6D, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(
|
||||
1, 1, 3D, 4, 32, 0.25D, 16, 0, 6, 1.6D, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(
|
||||
1, 1, 3D, 4, 32, 1.5D, 16, 2, 6, 1.6D, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(-1, 1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, -1, 3D, 4, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 0D, 4, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 3D, 40, 32, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 0.25D, 16, 2, 0, 1.6D, 2.5D, 24, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 0.25D, 16, 0, 6, 1.6D, 2.5D, 24, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 1.5D, 16, 2, 6, 1.6D, 2.5D, 24, true));
|
||||
assertThrows(IllegalArgumentException.class, () -> new HydrologyPlannerSettings.Surface(
|
||||
true,
|
||||
new HydrologyPlannerSettings.Source(true, 1D, 0, 0, 1, 0),
|
||||
|
||||
@@ -303,7 +303,7 @@ public class HydrologyTileCacheTest {
|
||||
|
||||
// Small-tile settings need a blend envelope that fits inside the bounded cross-tile admission period.
|
||||
private static HydrologyPlannerSettings.Banks tileBoundedBanks() {
|
||||
return new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 4, 0.25D, 16, 2, 6, 1.6D, true);
|
||||
return new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 4, 0.25D, 16, 2, 6, 1.6D, 2.5D, 24, true);
|
||||
}
|
||||
|
||||
private HydrologyTerrainSample featureTerrain(int x, int z) {
|
||||
|
||||
+27
-6
@@ -8,6 +8,7 @@ import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.IntBinaryOperator;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
@@ -20,7 +21,7 @@ public class ChannelProfileBuilderTest {
|
||||
};
|
||||
|
||||
@Test
|
||||
public void cruiseWidthIsConstantAndTapersFromTheHeadwater() {
|
||||
public void headwaterOpensAsASpringPoolThatNarrowsToTheCruiseWidth() {
|
||||
SurfaceCenterline centerline = straight(400);
|
||||
ChannelProfile profile = builder(1D, 1D).build(centerline, "water", false);
|
||||
|
||||
@@ -28,11 +29,14 @@ public class ChannelProfileBuilderTest {
|
||||
assertEquals(3D, profile.halfWidth()[200], 1.0E-9D);
|
||||
assertEquals(3D, profile.halfWidth()[399], 1.0E-9D);
|
||||
assertEquals(3D, profile.depth()[200], 1.0E-9D);
|
||||
assertTrue(profile.halfWidth()[0] < 1D);
|
||||
for (int station = 1; station < 48; station++) {
|
||||
assertTrue(profile.halfWidth()[station] >= profile.halfWidth()[station - 1]);
|
||||
assertEquals(3D * 2.5D, profile.halfWidth()[0], 1.0E-9D);
|
||||
assertEquals(4D, profile.depth()[0], 1.0E-9D);
|
||||
for (int station = 1; station <= 24; station++) {
|
||||
assertTrue(profile.halfWidth()[station] <= profile.halfWidth()[station - 1]);
|
||||
assertTrue(profile.depth()[station] <= profile.depth()[station - 1]);
|
||||
}
|
||||
assertTrue(profile.depth()[0] >= 1D);
|
||||
assertEquals(3D, profile.halfWidth()[24], 1.0E-9D);
|
||||
assertEquals(3D, profile.depth()[24], 1.0E-9D);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -55,9 +59,26 @@ public class ChannelProfileBuilderTest {
|
||||
assertEquals(6D * 1.25D + 2D, profile.collar(200, 0.25D), 1.0E-9D);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void groundFallingAwayAcrossThePoolShrinksTheSpringPool() {
|
||||
SurfaceCenterline centerline = straight(400);
|
||||
ChannelProfile sloped = builder(1D, 1D, (int x, int z) -> 80 - Math.abs(z) / 2).build(centerline, "water", false);
|
||||
ChannelProfile cliff = builder(1D, 1D, (int x, int z) -> 80 - Math.abs(z) * 8).build(centerline, "water", false);
|
||||
|
||||
assertTrue(sloped.halfWidth()[0] > 3D);
|
||||
assertTrue(sloped.halfWidth()[0] < 3D * 2.5D);
|
||||
assertEquals(3D, cliff.halfWidth()[0], 1.0E-9D);
|
||||
assertEquals(4D, cliff.depth()[0], 1.0E-9D);
|
||||
assertEquals(3D, sloped.halfWidth()[24], 1.0E-9D);
|
||||
}
|
||||
|
||||
private static ChannelProfileBuilder builder(double widthMultiplier, double depthMultiplier) {
|
||||
return builder(widthMultiplier, depthMultiplier, (int x, int z) -> 80);
|
||||
}
|
||||
|
||||
private static ChannelProfileBuilder builder(double widthMultiplier, double depthMultiplier, IntBinaryOperator height) {
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> new HydrologyTerrainSample(
|
||||
80,
|
||||
height.applyAsInt(x, z),
|
||||
0D,
|
||||
false,
|
||||
false,
|
||||
|
||||
+1
-1
@@ -173,7 +173,7 @@ public class ErosionFieldCompilerTest {
|
||||
true,
|
||||
defaults.sources(),
|
||||
4, 8, 2, 4, 10, 1.5D,
|
||||
new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 0D, 16, 2, 6, 1.6D, true)
|
||||
new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 0D, 16, 2, 6, 1.6D, 2.5D, 24, true)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user