mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-25 21:40:05 +00:00
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:
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;
|
package art.arcane.iris.engine.hydrology;
|
||||||
|
|
||||||
final class HydrologyHash {
|
public final class HydrologyHash {
|
||||||
private HydrologyHash() {
|
private HydrologyHash() {
|
||||||
}
|
}
|
||||||
|
|
||||||
static long mix(long seed, long... values) {
|
public static long mix(long seed, long... values) {
|
||||||
long mixed = avalanche(seed ^ 0x9e3779b97f4a7c15L);
|
long mixed = avalanche(seed ^ 0x9e3779b97f4a7c15L);
|
||||||
for (long value : values) {
|
for (long value : values) {
|
||||||
mixed = avalanche(mixed ^ avalanche(value + 0x9e3779b97f4a7c15L));
|
mixed = avalanche(mixed ^ avalanche(value + 0x9e3779b97f4a7c15L));
|
||||||
@@ -12,7 +12,7 @@ final class HydrologyHash {
|
|||||||
return mixed;
|
return mixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
static long text(String value) {
|
public static long text(String value) {
|
||||||
long hash = 0xcbf29ce484222325L;
|
long hash = 0xcbf29ce484222325L;
|
||||||
for (int index = 0; index < value.length(); index++) {
|
for (int index = 0; index < value.length(); index++) {
|
||||||
hash ^= value.charAt(index);
|
hash ^= value.charAt(index);
|
||||||
@@ -21,11 +21,11 @@ final class HydrologyHash {
|
|||||||
return avalanche(hash);
|
return avalanche(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
static double unit(long value) {
|
public static double unit(long value) {
|
||||||
return (avalanche(value) >>> 11) * 0x1.0p-53;
|
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) {
|
if (minimum > maximum) {
|
||||||
throw new IllegalArgumentException("minimum cannot exceed 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+115
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
+204
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+98
@@ -0,0 +1,98 @@
|
|||||||
|
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 org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ChannelProfileBuilderTest {
|
||||||
|
private static final HydrologyGeometrySampler CONSTANT_GEOMETRY = request -> switch (request.field()) {
|
||||||
|
case SURFACE_WIDTH -> 6;
|
||||||
|
case SURFACE_DEPTH -> 3;
|
||||||
|
default -> request.minimum();
|
||||||
|
};
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cruiseWidthIsConstantAndTapersFromTheHeadwater() {
|
||||||
|
SurfaceCenterline centerline = straight(400);
|
||||||
|
ChannelProfile profile = builder(1D, 1D).build(centerline, path(400), "water", false);
|
||||||
|
|
||||||
|
assertEquals(400, profile.size());
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
assertTrue(profile.depth()[0] >= 1D);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void directOceanCoursesFlareAtTheMouth() {
|
||||||
|
SurfaceCenterline centerline = straight(400);
|
||||||
|
ChannelProfile profile = builder(1D, 1D).build(centerline, path(400), "water", true);
|
||||||
|
|
||||||
|
assertEquals(3D * 1.6D, profile.halfWidth()[399], 0.05D);
|
||||||
|
assertEquals(3D, profile.halfWidth()[300], 1.0E-9D);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void policyMultipliersScaleWidthDepthAndBank() {
|
||||||
|
SurfaceCenterline centerline = straight(400);
|
||||||
|
ChannelProfile profile = builder(2D, 0.5D).build(centerline, path(400), "water", false);
|
||||||
|
|
||||||
|
assertEquals(6D, profile.halfWidth()[200], 1.0E-9D);
|
||||||
|
assertEquals(1.5D, profile.depth()[200], 1.0E-9D);
|
||||||
|
assertEquals(1.25D, profile.bankMultiplier()[200], 1.0E-9D);
|
||||||
|
assertEquals(6D * 1.25D + 2D, profile.collar(200, 0.25D), 1.0E-9D);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ChannelProfileBuilder builder(double widthMultiplier, double depthMultiplier) {
|
||||||
|
HydrologyTerrainSampler sampler = (int x, int z) -> new HydrologyTerrainSample(
|
||||||
|
80,
|
||||||
|
0D,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
48,
|
||||||
|
50,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
0D,
|
||||||
|
1D,
|
||||||
|
1D,
|
||||||
|
widthMultiplier,
|
||||||
|
depthMultiplier,
|
||||||
|
1D,
|
||||||
|
1D,
|
||||||
|
1.25D,
|
||||||
|
"parent",
|
||||||
|
"parent",
|
||||||
|
"parent",
|
||||||
|
"parent",
|
||||||
|
"parent",
|
||||||
|
"parent",
|
||||||
|
List.of("water")
|
||||||
|
);
|
||||||
|
return new ChannelProfileBuilder(HydrologyPlannerSettings.defaults().surface(), sampler, CONSTANT_GEOMETRY);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SurfaceCenterline straight(int stations) {
|
||||||
|
return SurfaceCenterline.densify(path(stations));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<HydrologyPoint> path(int stations) {
|
||||||
|
return List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(stations - 1, 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
+186
@@ -0,0 +1,186 @@
|
|||||||
|
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 art.arcane.iris.engine.hydrology.RiverFootprint;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ErosionFieldCompilerTest {
|
||||||
|
private static final int SEA_LEVEL = 60;
|
||||||
|
private static final HydrologyGeometrySampler CONSTANT_GEOMETRY = request -> switch (request.field()) {
|
||||||
|
case SURFACE_WIDTH -> 6;
|
||||||
|
case SURFACE_DEPTH -> 3;
|
||||||
|
default -> request.minimum();
|
||||||
|
};
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void flatTerrainProducesAContainedChannelWithLipAndNoWriteAboveNatural() {
|
||||||
|
Compiled compiled = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
|
||||||
|
|
||||||
|
assertEquals(0, compiled.field().uncontainedWetCells());
|
||||||
|
SurfaceColumn center = compiled.field().column(150, 0);
|
||||||
|
assertNotNull(center);
|
||||||
|
assertEquals(SurfaceRole.CHANNEL, center.role());
|
||||||
|
assertEquals(79, center.headY());
|
||||||
|
assertTrue(center.height() <= 76);
|
||||||
|
SurfaceColumn edge = compiled.field().column(150, 3);
|
||||||
|
assertNotNull(edge);
|
||||||
|
assertEquals(SurfaceRole.CHANNEL, edge.role());
|
||||||
|
assertTrue(edge.height() > center.height());
|
||||||
|
assertTrue(edge.height() <= 78);
|
||||||
|
SurfaceColumn shore = compiled.field().column(150, 4);
|
||||||
|
assertNotNull(shore);
|
||||||
|
assertEquals(SurfaceRole.SHORE, shore.role());
|
||||||
|
assertEquals(80, shore.height());
|
||||||
|
assertNoWriteAboveNatural(compiled);
|
||||||
|
assertChannelContained(compiled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void hillsideCutGetsABankWhoseWidthGrowsWithTheCut() {
|
||||||
|
Compiled compiled = compile(300, (x, z) -> 90 + Math.max(0, z) / 2, SurfaceTerminal.SINKHOLE, 40);
|
||||||
|
|
||||||
|
assertEquals(0, compiled.field().uncontainedWetCells());
|
||||||
|
assertNoWriteAboveNatural(compiled);
|
||||||
|
assertChannelContained(compiled);
|
||||||
|
int lastBank = -1;
|
||||||
|
int previous = Integer.MIN_VALUE;
|
||||||
|
for (int z = 4; z < 60; z++) {
|
||||||
|
SurfaceColumn column = compiled.field().column(150, z);
|
||||||
|
if (column == null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (column.role() == SurfaceRole.BANK) {
|
||||||
|
lastBank = z;
|
||||||
|
assertTrue(column.height() >= previous);
|
||||||
|
previous = column.height();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(lastBank > 12);
|
||||||
|
int stepOutsideChannel = 0;
|
||||||
|
for (int z = 6; z <= lastBank; z++) {
|
||||||
|
stepOutsideChannel = Math.max(
|
||||||
|
stepOutsideChannel,
|
||||||
|
Math.abs(compiled.field().column(150, z).height() - compiled.field().column(150, z - 1).height())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assertTrue(stepOutsideChannel <= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void slopingTerrainKeepsBanksSmoothAndContained() {
|
||||||
|
Compiled compiled = compile(400, (x, z) -> 120 - x / 8, SurfaceTerminal.SINKHOLE, 40);
|
||||||
|
|
||||||
|
assertEquals(0, compiled.field().uncontainedWetCells());
|
||||||
|
assertNoWriteAboveNatural(compiled);
|
||||||
|
assertChannelContained(compiled);
|
||||||
|
for (int x = 20; x < 380; x++) {
|
||||||
|
for (int z = 5; z < 12; z++) {
|
||||||
|
SurfaceColumn column = compiled.field().column(x, z);
|
||||||
|
SurfaceColumn next = compiled.field().column(x + 1, z);
|
||||||
|
if (column == null || next == null || column.role() == SurfaceRole.CHANNEL || next.role() == SurfaceRole.CHANNEL) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assertTrue(Math.abs(column.height() - next.height()) <= 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void oceanAndSubmergedCellsReceiveNoWrites() {
|
||||||
|
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 240 || z > 20
|
||||||
|
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||||
|
: HydrologyTerrainSample.openLand(z <= -20 ? SEA_LEVEL : 100 - x / 6, 0D, "land");
|
||||||
|
Compiled compiled = compile(sampler, 300, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||||
|
|
||||||
|
assertEquals(0, compiled.field().uncontainedWetCells());
|
||||||
|
for (SurfaceColumn column : compiled.field().columns().values()) {
|
||||||
|
HydrologyTerrainSample terrain = sampler.sample(column.x(), column.z());
|
||||||
|
if (terrain.ocean() || terrain.naturalHeight() <= SEA_LEVEL) {
|
||||||
|
assertTrue(column.apron());
|
||||||
|
assertEquals(terrain.naturalHeight(), column.height());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertNoWriteAboveNatural(compiled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bankColumnsBeyondTheBlendAreNotWritten() {
|
||||||
|
Compiled compiled = compile(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
|
||||||
|
|
||||||
|
assertNull(compiled.field().column(150, 40));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertNoWriteAboveNatural(Compiled compiled) {
|
||||||
|
for (SurfaceColumn column : compiled.field().columns().values()) {
|
||||||
|
assertTrue(column.height() <= column.terrain().naturalHeight());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertChannelContained(Compiled compiled) {
|
||||||
|
for (SurfaceColumn column : compiled.field().columns().values()) {
|
||||||
|
if (column.role() != SurfaceRole.CHANNEL || column.apron()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
|
||||||
|
for (int[] offset : offsets) {
|
||||||
|
SurfaceColumn neighbour = compiled.field().column(column.x() + offset[0], column.z() + offset[1]);
|
||||||
|
if (neighbour == null) {
|
||||||
|
assertEquals(SEA_LEVEL, column.headY());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (neighbour.role() == SurfaceRole.CHANNEL) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assertTrue(neighbour.height() >= column.headY());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Compiled compile(int stations, IntBinaryOperator height, SurfaceTerminal terminal, int terminalHead) {
|
||||||
|
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(height.applyAsInt(x, z), 0D, "land");
|
||||||
|
return compile(sampler, stations, terminal, terminalHead);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Compiled compile(HydrologyTerrainSampler sampler, int stations, SurfaceTerminal terminal, int terminalHead) {
|
||||||
|
HydrologyPlannerSettings.Surface surface = zeroRoughnessSurface();
|
||||||
|
List<HydrologyPoint> path = List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(stations - 1, 0, 0));
|
||||||
|
SurfaceCenterline centerline = SurfaceCenterline.densify(path);
|
||||||
|
ChannelProfile channel = new ChannelProfileBuilder(surface, sampler, CONSTANT_GEOMETRY)
|
||||||
|
.build(centerline, path, "water", terminal == SurfaceTerminal.OCEAN_MOUTH);
|
||||||
|
ValleyProfile valley = new ValleyProfileSolver(surface, sampler, SEA_LEVEL, 64)
|
||||||
|
.solve(centerline, channel, terminal, terminalHead);
|
||||||
|
assertNull(valley.rejection());
|
||||||
|
ErosionField field = new ErosionFieldCompiler(surface, sampler, SEA_LEVEL)
|
||||||
|
.compile(1234L, centerline, channel, valley, terminal, 8);
|
||||||
|
return new Compiled(field, valley);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HydrologyPlannerSettings.Surface zeroRoughnessSurface() {
|
||||||
|
HydrologyPlannerSettings.Surface defaults = HydrologyPlannerSettings.defaults().surface();
|
||||||
|
return new HydrologyPlannerSettings.Surface(
|
||||||
|
true,
|
||||||
|
defaults.sources(),
|
||||||
|
4, 8, 2, 4, 1, 1, 10, 1.5D, 4, 32, false, 1, 1,
|
||||||
|
new HydrologyPlannerSettings.Banks(1, 1, 3D, 4, 32, 0D, 16, 2, 6, 1.6D, true)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private record Compiled(ErosionField field, ValleyProfile valley) {
|
||||||
|
}
|
||||||
|
|
||||||
|
static long key(int x, int z) {
|
||||||
|
return RiverFootprint.pack(x, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology.surface;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class SurfaceCellAdmissionTest {
|
||||||
|
@Test
|
||||||
|
public void landAboveSeaLevelIsWritable() {
|
||||||
|
assertTrue(SurfaceCellAdmission.writable(HydrologyTerrainSample.openLand(61, 0D, "land"), 60));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void oceanSubmergedAndMissingSamplesAreNotWritable() {
|
||||||
|
assertFalse(SurfaceCellAdmission.writable(HydrologyTerrainSample.ocean(90, "ocean"), 60));
|
||||||
|
assertFalse(SurfaceCellAdmission.writable(HydrologyTerrainSample.openLand(60, 0D, "land"), 60));
|
||||||
|
assertFalse(SurfaceCellAdmission.writable(HydrologyTerrainSample.openLand(40, 0D, "land"), 60));
|
||||||
|
assertFalse(SurfaceCellAdmission.writable(null, 60));
|
||||||
|
}
|
||||||
|
}
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology.surface;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.hydrology.HydrologyPoint;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class SurfaceCenterlineTest {
|
||||||
|
@Test
|
||||||
|
public void straightPathDensifiesToOneBlockStationsWithAxisTangent() {
|
||||||
|
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(
|
||||||
|
new HydrologyPoint(0, 0, 0),
|
||||||
|
new HydrologyPoint(8, 0, 0)
|
||||||
|
));
|
||||||
|
|
||||||
|
assertEquals(9, centerline.size());
|
||||||
|
for (int station = 0; station < centerline.size(); station++) {
|
||||||
|
assertEquals(station, centerline.x()[station]);
|
||||||
|
assertEquals(0, centerline.z()[station]);
|
||||||
|
assertEquals(1D, centerline.tangentX()[station], 1.0E-9D);
|
||||||
|
assertEquals(0D, centerline.tangentZ()[station], 1.0E-9D);
|
||||||
|
assertEquals(0, centerline.pathIndex()[station]);
|
||||||
|
}
|
||||||
|
assertEquals(0D, centerline.normalX(4), 1.0E-9D);
|
||||||
|
assertEquals(1D, centerline.normalZ(4), 1.0E-9D);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void diagonalAndRepeatedPointsProduceNoDuplicateStations() {
|
||||||
|
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(
|
||||||
|
new HydrologyPoint(0, 0, 0),
|
||||||
|
new HydrologyPoint(4, 0, 4),
|
||||||
|
new HydrologyPoint(4, 0, 4),
|
||||||
|
new HydrologyPoint(4, 0, 9)
|
||||||
|
));
|
||||||
|
|
||||||
|
HashSet<Long> seen = new HashSet<>();
|
||||||
|
for (int station = 0; station < centerline.size(); station++) {
|
||||||
|
long key = ((long) centerline.x()[station] << 32) ^ (centerline.z()[station] & 0xffffffffL);
|
||||||
|
assertTrue(seen.add(key));
|
||||||
|
}
|
||||||
|
assertEquals(10, centerline.size());
|
||||||
|
assertEquals(0, centerline.pathIndex()[0]);
|
||||||
|
assertEquals(2, centerline.pathIndex()[9]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void distanceToSegmentMeasuresPerpendicularOffsetBesideAStraightRun() {
|
||||||
|
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(
|
||||||
|
new HydrologyPoint(0, 0, 0),
|
||||||
|
new HydrologyPoint(20, 0, 0)
|
||||||
|
));
|
||||||
|
|
||||||
|
assertEquals(3D, centerline.distanceToSegment(7, 7.5D, 3D), 1.0E-9D);
|
||||||
|
assertEquals(0D, centerline.distanceToSegment(7, 7.5D, 0D), 1.0E-9D);
|
||||||
|
assertEquals(2D, centerline.distanceToSegment(20, 22D, 0D), 1.0E-9D);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void truncateKeepsTheLeadingStations() {
|
||||||
|
SurfaceCenterline centerline = SurfaceCenterline.densify(List.of(
|
||||||
|
new HydrologyPoint(0, 0, 0),
|
||||||
|
new HydrologyPoint(20, 0, 0)
|
||||||
|
));
|
||||||
|
|
||||||
|
SurfaceCenterline truncated = centerline.truncate(5);
|
||||||
|
|
||||||
|
assertEquals(5, truncated.size());
|
||||||
|
assertEquals(4, truncated.x()[4]);
|
||||||
|
assertEquals(1D, truncated.tangentX()[4], 1.0E-9D);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology.surface;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class SurfaceNoiseTest {
|
||||||
|
@Test
|
||||||
|
public void valuesStayInRangeAndAreDeterministic() {
|
||||||
|
for (int x = -40; x < 40; x++) {
|
||||||
|
for (int z = -40; z < 40; z++) {
|
||||||
|
double value = SurfaceNoise.value(77L, x, z, 16);
|
||||||
|
assertTrue(value >= 0D && value <= 1D);
|
||||||
|
assertEquals(value, SurfaceNoise.value(77L, x, z, 16), 0D);
|
||||||
|
double signed = SurfaceNoise.signed(77L, x, z, 16);
|
||||||
|
assertTrue(signed >= -1D && signed <= 1D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void neighbouringCellsAreCoherent() {
|
||||||
|
double maximumStep = 0D;
|
||||||
|
for (int x = 0; x < 200; x++) {
|
||||||
|
maximumStep = Math.max(maximumStep, Math.abs(SurfaceNoise.value(5L, x, 3, 16) - SurfaceNoise.value(5L, x + 1, 3, 16)));
|
||||||
|
}
|
||||||
|
assertTrue(maximumStep < 0.2D);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void smoothStepEasesBetweenZeroAndOne() {
|
||||||
|
assertEquals(0D, SurfaceNoise.smoothStep(0D), 0D);
|
||||||
|
assertEquals(1D, SurfaceNoise.smoothStep(1D), 0D);
|
||||||
|
assertEquals(0.5D, SurfaceNoise.smoothStep(0.5D), 1.0E-9D);
|
||||||
|
assertTrue(SurfaceNoise.smoothStep(0.25D) < 0.25D);
|
||||||
|
assertTrue(SurfaceNoise.smoothStep(0.75D) > 0.75D);
|
||||||
|
}
|
||||||
|
}
|
||||||
+175
@@ -0,0 +1,175 @@
|
|||||||
|
package art.arcane.iris.engine.hydrology.surface;
|
||||||
|
|
||||||
|
import art.arcane.iris.engine.hydrology.HydrologyCandidateRejection;
|
||||||
|
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 org.junit.Test;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.IntBinaryOperator;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
|
public class ValleyProfileSolverTest {
|
||||||
|
private static final int SEA_LEVEL = 60;
|
||||||
|
private static final HydrologyGeometrySampler CONSTANT_GEOMETRY = request -> switch (request.field()) {
|
||||||
|
case SURFACE_WIDTH -> 6;
|
||||||
|
case SURFACE_DEPTH -> 2;
|
||||||
|
default -> request.minimum();
|
||||||
|
};
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void flatTerrainHoldsTheHeadOneInsetBelowTheBanks() {
|
||||||
|
ValleyProfile valley = solve(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 40);
|
||||||
|
|
||||||
|
assertNull(valley.rejection());
|
||||||
|
assertEquals(300, valley.exposedStations());
|
||||||
|
for (int station = 0; station < valley.exposedStations(); station++) {
|
||||||
|
assertEquals(79, valley.head()[station]);
|
||||||
|
assertEquals(80, valley.crossMin()[station]);
|
||||||
|
assertEquals(80, valley.centerNatural()[station]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void gentleSlopeProducesOneBlockNonRisingSteps() {
|
||||||
|
ValleyProfile valley = solve(400, (x, z) -> 120 - x / 8, SurfaceTerminal.SINKHOLE, 40);
|
||||||
|
|
||||||
|
assertNull(valley.rejection());
|
||||||
|
int steps = 0;
|
||||||
|
for (int station = 1; station < valley.exposedStations(); station++) {
|
||||||
|
int drop = valley.head()[station - 1] - valley.head()[station];
|
||||||
|
assertTrue(drop >= 0);
|
||||||
|
assertTrue(drop <= 1);
|
||||||
|
steps += drop;
|
||||||
|
}
|
||||||
|
assertTrue(steps >= 45 && steps <= 51);
|
||||||
|
assertContained(valley);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void hillsideTraverseKeepsTheHeadBelowTheLowestBank() {
|
||||||
|
ValleyProfile valley = solve(300, (x, z) -> 90 + z, SurfaceTerminal.SINKHOLE, 40);
|
||||||
|
|
||||||
|
assertNull(valley.rejection());
|
||||||
|
assertContained(valley);
|
||||||
|
assertTrue(valley.head()[150] < valley.centerNatural()[150] - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aDipBesideTheChannelLowersEveryDownstreamHead() {
|
||||||
|
ValleyProfile valley = solve(300, (x, z) -> x == 150 && z == 6 ? 74 : 80, SurfaceTerminal.SINKHOLE, 40);
|
||||||
|
|
||||||
|
assertNull(valley.rejection());
|
||||||
|
assertEquals(79, valley.head()[100]);
|
||||||
|
assertTrue(valley.head()[150] <= 73);
|
||||||
|
assertTrue(valley.head()[299] <= 73);
|
||||||
|
assertContained(valley);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void oceanTruncatesTheCourseAndPinsTheMouthToSeaLevel() {
|
||||||
|
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 240
|
||||||
|
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||||
|
: HydrologyTerrainSample.openLand(100 - x / 6, 0D, "land");
|
||||||
|
ValleyProfile valley = solver(sampler).solve(
|
||||||
|
straight(300),
|
||||||
|
channel(300, sampler),
|
||||||
|
SurfaceTerminal.OCEAN_MOUTH,
|
||||||
|
SEA_LEVEL
|
||||||
|
);
|
||||||
|
|
||||||
|
assertNull(valley.rejection());
|
||||||
|
assertTrue(valley.exposedStations() > 200);
|
||||||
|
assertTrue(valley.exposedStations() <= 240);
|
||||||
|
assertEquals(SEA_LEVEL, valley.head()[valley.exposedStations() - 1]);
|
||||||
|
assertEquals(SEA_LEVEL, valley.head()[valley.exposedStations()]);
|
||||||
|
assertEquals(SEA_LEVEL, valley.head()[299]);
|
||||||
|
for (int station = 0; station < valley.exposedStations(); station++) {
|
||||||
|
assertTrue(valley.head()[station] >= SEA_LEVEL);
|
||||||
|
}
|
||||||
|
assertContained(valley);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void landAtSeaLevelAlsoEndsTheExposedCourse() {
|
||||||
|
ValleyProfile valley = solve(300, (x, z) -> x >= 200 ? SEA_LEVEL : 90, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||||
|
|
||||||
|
assertNull(valley.rejection());
|
||||||
|
assertTrue(valley.exposedStations() <= 200);
|
||||||
|
assertTrue(valley.exposedStations() >= 190);
|
||||||
|
assertEquals(89, valley.head()[valley.exposedStations() - 1]);
|
||||||
|
assertEquals(SEA_LEVEL, valley.head()[valley.exposedStations()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void aCliffBecomesASingleLargeDrop() {
|
||||||
|
ValleyProfile valley = solve(300, (x, z) -> x < 150 ? 100 : 90, SurfaceTerminal.SINKHOLE, 40);
|
||||||
|
|
||||||
|
assertNull(valley.rejection());
|
||||||
|
int largest = 0;
|
||||||
|
for (int station = 1; station < valley.exposedStations(); station++) {
|
||||||
|
largest = Math.max(largest, valley.head()[station - 1] - valley.head()[station]);
|
||||||
|
}
|
||||||
|
assertEquals(10, largest);
|
||||||
|
assertEquals(99, valley.head()[100]);
|
||||||
|
assertEquals(89, valley.head()[200]);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void submergedCourseIsRejectedAsTooShort() {
|
||||||
|
ValleyProfile valley = solve(300, (x, z) -> 50, SurfaceTerminal.OCEAN_MOUTH, SEA_LEVEL);
|
||||||
|
|
||||||
|
assertEquals(HydrologyCandidateRejection.COURSE_TOO_SHORT, valley.rejection());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void sinkholeClearanceIsRequiredAtTheTerminal() {
|
||||||
|
ValleyProfile valley = solve(300, (x, z) -> 80, SurfaceTerminal.SINKHOLE, 90);
|
||||||
|
|
||||||
|
assertEquals(HydrologyCandidateRejection.SURFACE_SINKHOLE_CLEARANCE, valley.rejection());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void excessiveIncisionIsRejected() {
|
||||||
|
ValleyProfile valley = solve(300, (x, z) -> 90 - Math.min(20, Math.abs(z) * 8), SurfaceTerminal.SINKHOLE, 40);
|
||||||
|
|
||||||
|
assertEquals(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED, valley.rejection());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertContained(ValleyProfile valley) {
|
||||||
|
for (int station = 0; station < valley.exposedStations(); station++) {
|
||||||
|
assertTrue(valley.head()[station] <= valley.crossMin()[station] - 1);
|
||||||
|
if (station > 0) {
|
||||||
|
assertTrue(valley.head()[station] <= valley.head()[station - 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ValleyProfile solve(int stations, IntBinaryOperator height, SurfaceTerminal terminal, int terminalHead) {
|
||||||
|
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(height.applyAsInt(x, z), 0D, "land");
|
||||||
|
return solver(sampler).solve(straight(stations), channel(stations, sampler), terminal, terminalHead);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ValleyProfileSolver solver(HydrologyTerrainSampler sampler) {
|
||||||
|
return new ValleyProfileSolver(HydrologyPlannerSettings.defaults().surface(), sampler, SEA_LEVEL, 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ChannelProfile channel(int stations, HydrologyTerrainSampler sampler) {
|
||||||
|
return new ChannelProfileBuilder(HydrologyPlannerSettings.defaults().surface(), sampler, CONSTANT_GEOMETRY)
|
||||||
|
.build(straight(stations), path(stations), "water", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SurfaceCenterline straight(int stations) {
|
||||||
|
return SurfaceCenterline.densify(path(stations));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<HydrologyPoint> path(int stations) {
|
||||||
|
return List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(stations - 1, 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user