mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-28 23:10:32 +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);
|
||||
|
||||
Reference in new issue
Block a user