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