Surface valley model

- Add the surface package with centerline densification, channel profile, valley head solver and erosion field
- Derive the head from the bank ring outside the channel so every wet cell is contained by construction
- Stamp one continuous cross-section with a cut-scaled blend and a freeboard lip, filtered at the ocean
This commit is contained in:
Brian Neumann-Fopiano committed 2026-09-01 18:56:14 -04:00
1 parent 915cc1a2f0
commit 2c6792977f
19 files changed
+1296 -5

No files matched your search

@@ -1,10 +1,10 @@
package art.arcane.iris.engine.hydrology;
final class HydrologyHash {
public final class HydrologyHash {
private HydrologyHash() {
}
static long mix(long seed, long... values) {
public static long mix(long seed, long... values) {
long mixed = avalanche(seed ^ 0x9e3779b97f4a7c15L);
for (long value : values) {
mixed = avalanche(mixed ^ avalanche(value + 0x9e3779b97f4a7c15L));
@@ -12,7 +12,7 @@ final class HydrologyHash {
return mixed;
}
static long text(String value) {
public static long text(String value) {
long hash = 0xcbf29ce484222325L;
for (int index = 0; index < value.length(); index++) {
hash ^= value.charAt(index);
@@ -21,11 +21,11 @@ final class HydrologyHash {
return avalanche(hash);
}
static double unit(long value) {
public static double unit(long value) {
return (avalanche(value) >>> 11) * 0x1.0p-53;
}
static int between(long value, int minimum, int maximum) {
public static int between(long value, int minimum, int maximum) {
if (minimum > maximum) {
throw new IllegalArgumentException("minimum cannot exceed maximum.");
}
@@ -0,0 +1,11 @@
package art.arcane.iris.engine.hydrology.surface;
public record ChannelProfile(double[] halfWidth, double[] depth, double[] bankMultiplier) {
public int size() {
return halfWidth.length;
}
public double collar(int station, double roughness) {
return halfWidth[station] * (1D + roughness) + 2D;
}
}
@@ -0,0 +1,115 @@
package art.arcane.iris.engine.hydrology.surface;
import art.arcane.iris.engine.hydrology.HydrologyGeometrySampler;
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.hydrology.HydrologyPoint;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
import java.util.List;
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;
private final HydrologyTerrainSampler sampler;
private final HydrologyGeometrySampler geometry;
public ChannelProfileBuilder(
HydrologyPlannerSettings.Surface surface,
HydrologyTerrainSampler sampler,
HydrologyGeometrySampler geometry
) {
this.surface = Objects.requireNonNull(surface, "surface");
this.sampler = Objects.requireNonNull(sampler, "sampler");
this.geometry = Objects.requireNonNull(geometry, "geometry");
}
public ChannelProfile build(
SurfaceCenterline centerline,
List<HydrologyPoint> path,
String profileKey,
boolean directOcean
) {
int count = centerline.size();
double[] width = new double[count];
double[] depth = new double[count];
double[] bank = new double[count];
for (int station = 0; station < count; station++) {
HydrologyPoint point = path.get(Math.min(path.size() - 1, centerline.pathIndex()[station]));
int sampledWidth = geometry.sample(
HydrologyGeometrySampler.Field.SURFACE_WIDTH,
profileKey,
point.x(),
point.z(),
0L,
surface.minimumWidth(),
surface.maximumWidth()
);
int sampledDepth = geometry.sample(
HydrologyGeometrySampler.Field.SURFACE_DEPTH,
profileKey,
point.x(),
point.z(),
0L,
surface.minimumDepth(),
surface.maximumDepth()
);
HydrologyTerrainSample terrain = sampler.sample(centerline.x()[station], centerline.z()[station]);
double widthMultiplier = terrain == null ? 1D : terrain.widthMultiplier();
double depthMultiplier = terrain == null ? 1D : terrain.depthMultiplier();
bank[station] = terrain == null ? 1D : terrain.bankMultiplier();
width[station] = clamp(sampledWidth * widthMultiplier, surface.minimumWidth(), surface.maximumWidth() * 2D);
depth[station] = clamp(sampledDepth * depthMultiplier, 1D, surface.maximumDepth() * 2D);
}
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;
}
if (directOcean) {
int flare = Math.min(MOUTH_FLARE, count / 3);
int flareStart = count - flare;
for (int station = Math.max(0, flareStart); station < count; station++) {
double progress = SurfaceNoise.smoothStep((station - flareStart) / (double) Math.max(1, flare - 1));
smoothWidth[station] *= 1D + (surface.banks().mouthFlareRatio() - 1D) * progress;
}
}
double[] halfWidth = new double[count];
for (int station = 0; station < count; station++) {
halfWidth[station] = Math.max(0.5D, smoothWidth[station] / 2D);
smoothDepth[station] = Math.max(1D, smoothDepth[station]);
}
return new ChannelProfile(halfWidth, smoothDepth, bank);
}
static double[] smooth(double[] values) {
int count = values.length;
double[] smoothed = new double[count];
for (int station = 0; station < count; station++) {
double total = 0D;
double weight = 0D;
for (int offset = -SMOOTHING_RADIUS; offset <= SMOOTHING_RADIUS; offset++) {
int index = station + offset;
if (index < 0 || index >= count) {
continue;
}
double factor = SMOOTHING_RADIUS + 1 - Math.abs(offset);
total += values[index] * factor;
weight += factor;
}
smoothed[station] = total / weight;
}
return smoothed;
}
private static double clamp(double value, double minimum, double maximum) {
return Math.max(minimum, Math.min(maximum, value));
}
}
@@ -0,0 +1,32 @@
package art.arcane.iris.engine.hydrology.surface;
import art.arcane.iris.engine.hydrology.RiverFootprint;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.Objects;
public final class ErosionField {
private final Long2ObjectOpenHashMap<SurfaceColumn> columns;
private final int uncontainedWetCells;
ErosionField(Long2ObjectOpenHashMap<SurfaceColumn> columns, int uncontainedWetCells) {
this.columns = Objects.requireNonNull(columns, "columns");
this.uncontainedWetCells = uncontainedWetCells;
}
public Long2ObjectOpenHashMap<SurfaceColumn> columns() {
return columns;
}
public SurfaceColumn column(int x, int z) {
return columns.get(RiverFootprint.pack(x, z));
}
public int uncontainedWetCells() {
return uncontainedWetCells;
}
public int size() {
return columns.size();
}
}
@@ -0,0 +1,204 @@
package art.arcane.iris.engine.hydrology.surface;
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
import art.arcane.iris.engine.hydrology.RiverFootprint;
import it.unimi.dsi.fastutil.longs.Long2DoubleOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import java.util.Objects;
public final class ErosionFieldCompiler {
private static final int[][] CARDINALS = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private static final long OUTLINE_SALT = 0x4f55544c494e45L;
private static final long BED_SALT = 0x424544L;
private static final double EPSILON = 1.0E-9D;
private final HydrologyPlannerSettings.Surface surface;
private final HydrologyTerrainSampler sampler;
private final int seaLevel;
public ErosionFieldCompiler(
HydrologyPlannerSettings.Surface surface,
HydrologyTerrainSampler sampler,
int seaLevel
) {
this.surface = Objects.requireNonNull(surface, "surface");
this.sampler = Objects.requireNonNull(sampler, "sampler");
this.seaLevel = seaLevel;
}
public ErosionField compile(
long courseSeed,
SurfaceCenterline centerline,
ChannelProfile channel,
ValleyProfile valley,
SurfaceTerminal terminal,
int apronLimit
) {
HydrologyPlannerSettings.Banks banks = surface.banks();
int count = terminal == SurfaceTerminal.OCEAN_MOUTH ? centerline.size() : valley.exposedStations();
double roughness = banks.roughness();
double shoreWidth = surface.shoreWidth();
int freeboard = banks.freeboard();
boolean[] basin = basins(valley, channel, count);
double[][] blendWidth = blendWidths(centerline, channel, valley, count);
Long2IntOpenHashMap nearest = new Long2IntOpenHashMap();
nearest.defaultReturnValue(-1);
Long2DoubleOpenHashMap distance = new Long2DoubleOpenHashMap();
for (int station = 0; station < count; station++) {
double widest = Math.max(blendWidth[station][0], blendWidth[station][1]);
int radius = (int) StrictMath.ceil(channel.halfWidth()[station] * (1D + roughness) + shoreWidth + widest) + 1;
int stationX = centerline.x()[station];
int stationZ = centerline.z()[station];
for (int deltaZ = -radius; deltaZ <= radius; deltaZ++) {
for (int deltaX = -radius; deltaX <= radius; deltaX++) {
int cellX = stationX + deltaX;
int cellZ = stationZ + deltaZ;
double cellDistance = centerline.distanceToSegment(station, cellX, cellZ);
if (cellDistance > radius) {
continue;
}
long key = RiverFootprint.pack(cellX, cellZ);
int existing = nearest.get(key);
if (existing < 0
|| cellDistance < distance.get(key) - EPSILON
|| Math.abs(cellDistance - distance.get(key)) <= EPSILON && station < existing) {
nearest.put(key, station);
distance.put(key, cellDistance);
}
}
}
}
Long2ObjectOpenHashMap<SurfaceColumn> columns = new Long2ObjectOpenHashMap<>(nearest.size());
LongArrayList wetKeys = new LongArrayList();
for (long key : nearest.keySet()) {
int station = nearest.get(key);
double cellDistance = distance.get(key);
int cellX = RiverFootprint.unpackX(key);
int cellZ = RiverFootprint.unpackZ(key);
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
int head = valley.head()[station];
double halfWidth = channel.halfWidth()[station];
double outline = halfWidth * (1D + roughness * SurfaceNoise.signed(
courseSeed ^ OUTLINE_SALT, cellX, cellZ, banks.roughnessWavelength()));
outline = Math.max(Math.max(0.5D, 0.6D * halfWidth), Math.min(1.4D * halfWidth, outline));
boolean wet = cellDistance <= outline + 0.25D;
if (!SurfaceCellAdmission.writable(terrain, seaLevel)) {
if (terrain != null && wet && terminal == SurfaceTerminal.OCEAN_MOUTH
&& station >= valley.exposedStations() - 1
&& station - valley.exposedStations() < apronLimit) {
columns.put(key, new SurfaceColumn(
cellX, cellZ, terrain, station, SurfaceRole.CHANNEL, terrain.naturalHeight(), seaLevel, true));
}
continue;
}
int natural = terrain.naturalHeight();
if (wet) {
double normalized = Math.min(1D, cellDistance / Math.max(0.5D, outline));
double depth = channel.depth()[station];
double local = 1D + (depth - 1D) * (1D - normalized * normalized)
+ 0.5D * roughness * SurfaceNoise.signed(courseSeed ^ BED_SALT, cellX, cellZ, banks.roughnessWavelength())
+ (basin[station] ? 1D : 0D);
int bed = Math.min(natural, head - Math.max(1, (int) StrictMath.round(local)));
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.CHANNEL, bed, head, false));
wetKeys.add(key);
continue;
}
int bankTop = head + freeboard;
if (cellDistance <= outline + shoreWidth + 0.25D) {
int height = Math.min(natural, bankTop);
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.SHORE, height, height, false));
continue;
}
int cut = natural - bankTop;
if (cut <= 0) {
continue;
}
double side = side(centerline, station, cellX, cellZ);
double width = blendWidth[station][side < 0D ? 0 : 1];
double progress = (cellDistance - outline - shoreWidth) / width;
if (progress >= 1D) {
continue;
}
int height = Math.min(natural, (int) StrictMath.round(bankTop + cut * SurfaceNoise.smoothStep(progress)));
columns.put(key, new SurfaceColumn(cellX, cellZ, terrain, station, SurfaceRole.BANK, height, height, false));
}
int uncontained = contain(columns, wetKeys, freeboard);
return new ErosionField(columns, uncontained);
}
private int contain(Long2ObjectOpenHashMap<SurfaceColumn> columns, LongArrayList wetKeys, int freeboard) {
int uncontained = 0;
for (int index = 0; index < wetKeys.size(); index++) {
SurfaceColumn wet = columns.get(wetKeys.getLong(index));
for (int[] offset : CARDINALS) {
long key = RiverFootprint.pack(wet.x() + offset[0], wet.z() + offset[1]);
SurfaceColumn neighbour = columns.get(key);
if (neighbour == null) {
if (wet.headY() != seaLevel) {
uncontained++;
}
continue;
}
if (neighbour.role() == SurfaceRole.CHANNEL) {
continue;
}
int required = wet.headY() + freeboard;
if (neighbour.height() < required) {
int raised = Math.min(neighbour.terrain().naturalHeight(), required);
neighbour = neighbour.withHeight(raised);
columns.put(key, neighbour);
}
if (neighbour.height() < wet.headY()) {
uncontained++;
}
}
}
return uncontained;
}
private boolean[] basins(ValleyProfile valley, ChannelProfile channel, int count) {
boolean[] basin = new boolean[count];
for (int station = 1; station < count; station++) {
int drop = valley.head()[station - 1] - valley.head()[station];
if (drop < 2) {
continue;
}
int length = Math.max(2, (int) StrictMath.round(2D * channel.halfWidth()[station]));
for (int following = station; following < Math.min(count, station + length); following++) {
basin[following] = true;
}
}
return basin;
}
private double[][] blendWidths(SurfaceCenterline centerline, ChannelProfile channel, ValleyProfile valley, int count) {
HydrologyPlannerSettings.Banks banks = surface.banks();
double[][] widths = new double[count][2];
for (int station = 0; station < count; station++) {
double halfWidth = channel.halfWidth()[station];
double probe = halfWidth + surface.shoreWidth() + Math.max(2D, halfWidth);
int bankTop = valley.head()[station] + banks.freeboard();
for (int side = 0; side < 2; side++) {
double direction = side == 0 ? -1D : 1D;
int probeX = (int) StrictMath.round(centerline.x()[station] + centerline.normalX(station) * probe * direction);
int probeZ = (int) StrictMath.round(centerline.z()[station] + centerline.normalZ(station) * probe * direction);
HydrologyTerrainSample terrain = sampler.sample(probeX, probeZ);
double cut = terrain == null ? 0D : Math.max(0D, terrain.naturalHeight() - bankTop);
double width = cut * banks.blendSlope() * channel.bankMultiplier()[station];
widths[station][side] = Math.max(banks.minimumBlendWidth(), Math.min(banks.maximumBlendWidth(), width));
}
}
return widths;
}
private static double side(SurfaceCenterline centerline, int station, int cellX, int cellZ) {
double deltaX = cellX - centerline.x()[station];
double deltaZ = cellZ - centerline.z()[station];
return deltaX * centerline.normalX(station) + deltaZ * centerline.normalZ(station);
}
}
@@ -0,0 +1,12 @@
package art.arcane.iris.engine.hydrology.surface;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
public final class SurfaceCellAdmission {
private SurfaceCellAdmission() {
}
public static boolean writable(HydrologyTerrainSample terrain, int seaLevel) {
return terrain != null && !terrain.ocean() && terrain.naturalHeight() > seaLevel;
}
}
@@ -0,0 +1,109 @@
package art.arcane.iris.engine.hydrology.surface;
import art.arcane.iris.engine.hydrology.HydrologyPoint;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import java.util.List;
import java.util.Objects;
public record SurfaceCenterline(int[] x, int[] z, double[] tangentX, double[] tangentZ, int[] pathIndex) {
private static final int TANGENT_WINDOW = 6;
public static SurfaceCenterline densify(List<HydrologyPoint> path) {
Objects.requireNonNull(path, "path");
if (path.isEmpty()) {
throw new IllegalArgumentException("A surface centerline requires at least one path point.");
}
IntArrayList xs = new IntArrayList();
IntArrayList zs = new IntArrayList();
IntArrayList indices = new IntArrayList();
HydrologyPoint first = path.getFirst();
xs.add(first.x());
zs.add(first.z());
indices.add(0);
for (int pair = 0; pair + 1 < path.size(); pair++) {
HydrologyPoint start = path.get(pair);
HydrologyPoint end = path.get(pair + 1);
int steps = Math.max(Math.abs(end.x() - start.x()), Math.abs(end.z() - start.z()));
for (int step = 1; step <= steps; step++) {
double progress = step / (double) steps;
int cellX = (int) StrictMath.round(start.x() + (end.x() - start.x()) * progress);
int cellZ = (int) StrictMath.round(start.z() + (end.z() - start.z()) * progress);
int last = xs.size() - 1;
if (xs.getInt(last) == cellX && zs.getInt(last) == cellZ) {
continue;
}
xs.add(cellX);
zs.add(cellZ);
indices.add(pair);
}
}
return fromStations(xs.toIntArray(), zs.toIntArray(), indices.toIntArray());
}
private static SurfaceCenterline fromStations(int[] xs, int[] zs, int[] indices) {
int count = xs.length;
double[] tangentX = new double[count];
double[] tangentZ = new double[count];
for (int station = 0; station < count; station++) {
int from = Math.max(0, station - TANGENT_WINDOW);
int to = Math.min(count - 1, station + TANGENT_WINDOW);
double vectorX = xs[to] - xs[from];
double vectorZ = zs[to] - zs[from];
double length = StrictMath.hypot(vectorX, vectorZ);
if (length <= 0D) {
tangentX[station] = 1D;
tangentZ[station] = 0D;
} else {
tangentX[station] = vectorX / length;
tangentZ[station] = vectorZ / length;
}
}
return new SurfaceCenterline(xs, zs, tangentX, tangentZ, indices);
}
public int size() {
return x.length;
}
public double normalX(int station) {
return -tangentZ[station];
}
public double normalZ(int station) {
return tangentX[station];
}
public double distanceToSegment(int station, double pointX, double pointZ) {
double startX = x[station];
double startZ = z[station];
if (station + 1 >= x.length) {
return StrictMath.hypot(pointX - startX, pointZ - startZ);
}
double endX = x[station + 1];
double endZ = z[station + 1];
double segmentX = endX - startX;
double segmentZ = endZ - startZ;
double lengthSquared = segmentX * segmentX + segmentZ * segmentZ;
double progress = lengthSquared <= 0D
? 0D
: ((pointX - startX) * segmentX + (pointZ - startZ) * segmentZ) / lengthSquared;
progress = Math.max(0D, Math.min(1D, progress));
double closestX = startX + segmentX * progress;
double closestZ = startZ + segmentZ * progress;
return StrictMath.hypot(pointX - closestX, pointZ - closestZ);
}
public SurfaceCenterline truncate(int stations) {
if (stations < 1 || stations > x.length) {
throw new IllegalArgumentException("Cannot truncate a centerline of " + x.length + " stations to " + stations + ".");
}
int[] xs = new int[stations];
int[] zs = new int[stations];
int[] indices = new int[stations];
System.arraycopy(x, 0, xs, 0, stations);
System.arraycopy(z, 0, zs, 0, stations);
System.arraycopy(pathIndex, 0, indices, 0, stations);
return fromStations(xs, zs, indices);
}
}
@@ -0,0 +1,25 @@
package art.arcane.iris.engine.hydrology.surface;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
import java.util.Objects;
public record SurfaceColumn(
int x,
int z,
HydrologyTerrainSample terrain,
int station,
SurfaceRole role,
int height,
int headY,
boolean apron
) {
public SurfaceColumn {
Objects.requireNonNull(terrain, "terrain");
Objects.requireNonNull(role, "role");
}
public SurfaceColumn withHeight(int replacementHeight) {
return new SurfaceColumn(x, z, terrain, station, role, replacementHeight, replacementHeight, apron);
}
}
@@ -0,0 +1,36 @@
package art.arcane.iris.engine.hydrology.surface;
import art.arcane.iris.engine.hydrology.HydrologyHash;
public final class SurfaceNoise {
private SurfaceNoise() {
}
public static double value(long seed, int x, int z, int wavelength) {
int span = Math.max(1, wavelength);
int cellX = Math.floorDiv(x, span);
int cellZ = Math.floorDiv(z, span);
double localX = smoothStep(Math.floorMod(x, span) / (double) span);
double localZ = smoothStep(Math.floorMod(z, span) / (double) span);
double top = lerp(corner(seed, cellX, cellZ), corner(seed, cellX + 1, cellZ), localX);
double bottom = lerp(corner(seed, cellX, cellZ + 1), corner(seed, cellX + 1, cellZ + 1), localX);
return lerp(top, bottom, localZ);
}
public static double signed(long seed, int x, int z, int wavelength) {
return value(seed, x, z, wavelength) * 2D - 1D;
}
public static double smoothStep(double progress) {
double clamped = Math.max(0D, Math.min(1D, progress));
return clamped * clamped * (3D - 2D * clamped);
}
private static double corner(long seed, int cellX, int cellZ) {
return HydrologyHash.unit(HydrologyHash.mix(seed, cellX, cellZ));
}
private static double lerp(double first, double second, double progress) {
return first + (second - first) * progress;
}
}
@@ -0,0 +1,7 @@
package art.arcane.iris.engine.hydrology.surface;
public enum SurfaceRole {
CHANNEL,
SHORE,
BANK
}
@@ -0,0 +1,7 @@
package art.arcane.iris.engine.hydrology.surface;
public enum SurfaceTerminal {
OCEAN_MOUTH,
SINKHOLE,
COASTAL_GROTTO
}
@@ -0,0 +1,20 @@
package art.arcane.iris.engine.hydrology.surface;
import art.arcane.iris.engine.hydrology.HydrologyCandidateRejection;
public record ValleyProfile(
int[] head,
int[] crossMin,
int[] crossMax,
int[] centerNatural,
int exposedStations,
HydrologyCandidateRejection rejection
) {
public boolean accepted() {
return rejection == null;
}
public static ValleyProfile rejected(HydrologyCandidateRejection rejection) {
return new ValleyProfile(new int[0], new int[0], new int[0], new int[0], 0, rejection);
}
}
@@ -0,0 +1,117 @@
package art.arcane.iris.engine.hydrology.surface;
import art.arcane.iris.engine.hydrology.HydrologyCandidateRejection;
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
import art.arcane.iris.engine.hydrology.HydrologyTerrainSampler;
import java.util.Objects;
public final class ValleyProfileSolver {
private final HydrologyPlannerSettings.Surface surface;
private final HydrologyTerrainSampler sampler;
private final int seaLevel;
private final int minimumCourseLength;
public ValleyProfileSolver(
HydrologyPlannerSettings.Surface surface,
HydrologyTerrainSampler sampler,
int seaLevel,
int minimumCourseLength
) {
this.surface = Objects.requireNonNull(surface, "surface");
this.sampler = Objects.requireNonNull(sampler, "sampler");
this.seaLevel = seaLevel;
this.minimumCourseLength = minimumCourseLength;
}
public ValleyProfile solve(
SurfaceCenterline centerline,
ChannelProfile channel,
SurfaceTerminal terminal,
int terminalHead
) {
int count = centerline.size();
int[] crossMin = new int[count];
int[] crossMax = new int[count];
int[] centerNatural = new int[count];
double[] incisionMultiplier = new double[count];
double roughness = surface.banks().roughness();
int exposed = count;
for (int station = 0; station < count; station++) {
double outline = channel.halfWidth()[station] * (1D + roughness);
double reach = outline + 2D;
int stationX = centerline.x()[station];
int stationZ = centerline.z()[station];
HydrologyTerrainSample center = sampler.sample(stationX, stationZ);
if (!SurfaceCellAdmission.writable(center, seaLevel)) {
exposed = station;
break;
}
centerNatural[station] = center.naturalHeight();
incisionMultiplier[station] = center.incisionMultiplier();
double normalX = centerline.normalX(station);
double normalZ = centerline.normalZ(station);
int minimum = Integer.MAX_VALUE;
int maximum = Integer.MIN_VALUE;
boolean blocked = false;
for (double offset = -reach; offset <= reach && !blocked; offset += 0.5D) {
int cellX = (int) StrictMath.round(stationX + normalX * offset);
int cellZ = (int) StrictMath.round(stationZ + normalZ * offset);
HydrologyTerrainSample terrain = sampler.sample(cellX, cellZ);
if (!SurfaceCellAdmission.writable(terrain, seaLevel)) {
blocked = true;
break;
}
if (Math.abs(offset) <= outline + 0.25D) {
continue;
}
minimum = Math.min(minimum, terrain.naturalHeight());
maximum = Math.max(maximum, terrain.naturalHeight());
}
if (blocked) {
exposed = station;
break;
}
crossMin[station] = minimum;
crossMax[station] = maximum;
}
if (exposed < 2 || exposed < minimumCourseLength) {
return ValleyProfile.rejected(HydrologyCandidateRejection.COURSE_TOO_SHORT);
}
if (exposed < count && terminal != SurfaceTerminal.OCEAN_MOUTH) {
return ValleyProfile.rejected(HydrologyCandidateRejection.SURFACE_EXPOSURE);
}
int inset = surface.banks().inset();
int[] head = new int[count];
for (int station = 0; station < exposed; station++) {
int ceiling = crossMin[station] - inset;
head[station] = station == 0 ? ceiling : Math.min(head[station - 1], ceiling);
}
if (terminal == SurfaceTerminal.OCEAN_MOUTH || terminal == SurfaceTerminal.COASTAL_GROTTO) {
for (int station = 0; station < exposed; station++) {
head[station] = Math.max(head[station], seaLevel);
}
}
if (terminal == SurfaceTerminal.OCEAN_MOUTH) {
for (int station = exposed; station < count; station++) {
head[station] = seaLevel;
}
}
if (terminal == SurfaceTerminal.SINKHOLE && head[exposed - 1] < terminalHead) {
return ValleyProfile.rejected(HydrologyCandidateRejection.SURFACE_SINKHOLE_CLEARANCE);
}
if (terminal == SurfaceTerminal.COASTAL_GROTTO && head[exposed - 1] < terminalHead) {
return ValleyProfile.rejected(HydrologyCandidateRejection.SURFACE_HEAD_RANGE);
}
int maximumIncision = surface.maximumIncision();
for (int station = 0; station < exposed; station++) {
int permitted = Math.min(maximumIncision, (int) StrictMath.floor(maximumIncision * incisionMultiplier[station]));
int bed = head[station] - (int) StrictMath.round(channel.depth()[station]);
if (centerNatural[station] - bed > permitted) {
return ValleyProfile.rejected(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED);
}
}
return new ValleyProfile(head, crossMin, crossMax, centerNatural, exposed, null);
}
}