mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-10-11 19:14:11 +00:00
Surface course integration
- Build surface courses through the valley solver and segment labeler - Compile surface footprints through the erosion field and index them for cave containment - Drop the contained-surface fallback and the post-hoc surface rejection passes - Choose the river profile at the source
This commit is contained in:
@@ -62,15 +62,6 @@ public record HydrologyColumnSample(
|
||||
}
|
||||
}
|
||||
}
|
||||
for (HydrologyColumnLayer layer : layers) {
|
||||
if (layer.feature().type().isSurface()
|
||||
&& !layer.oceanApron()
|
||||
&& layer.channel()
|
||||
&& layer.fluidOwned()
|
||||
&& layer.fluidHeadY() >= naturalHeight) {
|
||||
throw new IllegalArgumentException("Owned surface fluid must remain below natural terrain.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean present() {
|
||||
|
||||
+67
-213
@@ -1,5 +1,8 @@
|
||||
package art.arcane.iris.engine.hydrology;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprint;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceFootprintCompiler;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceLayerColumn;
|
||||
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||
@@ -49,6 +52,8 @@ final class HydrologyFootprintCompiler {
|
||||
private final LinkedHashMap<CourseRasterKey, RiverFootprint> courseFootprints;
|
||||
private final LinkedHashMap<CourseRasterKey, ValidationCourseRaster> validationCourseRasters;
|
||||
private final Map<RasterStencilKey, RasterStencil> rasterStencils;
|
||||
private final SurfaceFootprintCompiler surfaceCompiler;
|
||||
private final LinkedHashMap<CourseRasterKey, SurfaceFootprint> surfaceFootprints;
|
||||
private int fullMaterializationCount;
|
||||
|
||||
HydrologyFootprintCompiler(
|
||||
@@ -86,9 +91,30 @@ final class HydrologyFootprintCompiler {
|
||||
this.courseFootprints = new LinkedHashMap<>(COURSE_FOOTPRINT_CACHE_SIZE, 1F, true);
|
||||
this.validationCourseRasters = new LinkedHashMap<>(VALIDATION_RASTER_CACHE_SIZE, 1F, true);
|
||||
this.rasterStencils = new HashMap<>();
|
||||
this.surfaceCompiler = new SurfaceFootprintCompiler(settings, this::sampleTerrainBasis, this.geometrySampler);
|
||||
this.surfaceFootprints = new LinkedHashMap<>(COURSE_FOOTPRINT_CACHE_SIZE, 1F, true);
|
||||
this.fullMaterializationCount = 0;
|
||||
}
|
||||
|
||||
SurfaceFootprint surfaceFootprint(RiverCourse course) {
|
||||
CourseRasterKey rasterKey = new CourseRasterKey(
|
||||
course.id(),
|
||||
course.type(),
|
||||
course.profileKey(),
|
||||
course.segments()
|
||||
);
|
||||
SurfaceFootprint cached = surfaceFootprints.get(rasterKey);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
SurfaceFootprint compiled = surfaceCompiler.compile(course);
|
||||
surfaceFootprints.put(rasterKey, compiled);
|
||||
if (surfaceFootprints.size() > COURSE_FOOTPRINT_CACHE_SIZE) {
|
||||
surfaceFootprints.remove(surfaceFootprints.sequencedKeySet().getFirst());
|
||||
}
|
||||
return compiled;
|
||||
}
|
||||
|
||||
RiverFootprint compile(List<RiverCourse> courses) {
|
||||
fullMaterializationCount++;
|
||||
if (courses.isEmpty()) {
|
||||
@@ -110,7 +136,6 @@ final class HydrologyFootprintCompiler {
|
||||
column.merge(sample);
|
||||
}
|
||||
}
|
||||
containSurfaceBanks(columns);
|
||||
return build(columns);
|
||||
}
|
||||
|
||||
@@ -125,17 +150,17 @@ final class HydrologyFootprintCompiler {
|
||||
if (!caveCandidatePresent) {
|
||||
return new ValidationRaster(
|
||||
List.of(),
|
||||
new SurfaceRasterIndex(List.of(), new Long2ObjectOpenHashMap<>())
|
||||
new SurfaceRasterIndex(List.<SurfaceFootprint>of(), new Long2ObjectOpenHashMap<>())
|
||||
);
|
||||
}
|
||||
|
||||
ArrayList<ValidationCourseRaster> courseRasters = new ArrayList<>(courses.size());
|
||||
ArrayList<SurfaceSweep> surfaceSweeps = new ArrayList<>();
|
||||
ArrayList<SurfaceFootprint> surfaces = new ArrayList<>();
|
||||
Long2ObjectOpenHashMap<HydrologyColumnSample> firstSamples = new Long2ObjectOpenHashMap<>();
|
||||
for (RiverCourse course : courses) {
|
||||
ValidationCourseRaster raster = compileValidationCourse(course);
|
||||
courseRasters.add(raster);
|
||||
surfaceSweeps.addAll(raster.surfaceSweeps());
|
||||
surfaces.add(raster.surface());
|
||||
for (HydrologyColumnSample sample : raster.columns()) {
|
||||
long packed = RiverFootprint.pack(sample.x(), sample.z());
|
||||
HydrologyColumnSample first = firstSamples.putIfAbsent(packed, sample);
|
||||
@@ -146,7 +171,7 @@ final class HydrologyFootprintCompiler {
|
||||
}
|
||||
return new ValidationRaster(
|
||||
List.copyOf(courseRasters),
|
||||
new SurfaceRasterIndex(surfaceSweeps, firstSamples)
|
||||
new SurfaceRasterIndex(surfaces, firstSamples)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -567,14 +592,11 @@ final class HydrologyFootprintCompiler {
|
||||
boolean caveCourse = hasCaveSegment(course);
|
||||
features.clear();
|
||||
Long2ObjectLinkedOpenHashMap<MutableColumn> columns = new Long2ObjectLinkedOpenHashMap<>();
|
||||
List<SurfaceSweep> surfaceSweeps = surfaceSweeps(course, true);
|
||||
SurfaceRasterIndex courseSurface = new SurfaceRasterIndex(
|
||||
surfaceSweeps,
|
||||
new Long2ObjectOpenHashMap<>()
|
||||
);
|
||||
for (SurfaceSweep sweep : surfaceSweeps) {
|
||||
rasterizeSurfaceSweep(columns, sweep, true);
|
||||
SurfaceFootprint surface = surfaceFootprint(course);
|
||||
for (SurfaceLayerColumn column : surface.columns()) {
|
||||
addLayer(columns, column.x(), column.z(), column.terrain(), column.layer());
|
||||
}
|
||||
SurfaceRasterIndex courseSurface = new SurfaceRasterIndex(List.of(surface), new Long2ObjectOpenHashMap<>());
|
||||
for (int segmentIndex = 0; segmentIndex < course.segments().size(); segmentIndex++) {
|
||||
HydraulicSegment segment = course.segments().get(segmentIndex);
|
||||
boolean firstSegment = segmentIndex == 0;
|
||||
@@ -599,7 +621,7 @@ final class HydrologyFootprintCompiler {
|
||||
ValidationCourseRaster raster = new ValidationCourseRaster(
|
||||
course.id(),
|
||||
buildValidationColumns(columns),
|
||||
surfaceSweeps
|
||||
surface
|
||||
);
|
||||
validationCourseRasters.put(rasterKey, raster);
|
||||
if (validationCourseRasters.size() > VALIDATION_RASTER_CACHE_SIZE) {
|
||||
@@ -658,19 +680,14 @@ final class HydrologyFootprintCompiler {
|
||||
}
|
||||
features.clear();
|
||||
Long2ObjectLinkedOpenHashMap<MutableColumn> columns = new Long2ObjectLinkedOpenHashMap<>();
|
||||
List<SurfaceSweep> surfaceSweeps = surfaceSweeps(course, true);
|
||||
SurfaceRasterIndex courseSurface = new SurfaceRasterIndex(
|
||||
surfaceSweeps,
|
||||
new Long2ObjectOpenHashMap<>()
|
||||
);
|
||||
for (SurfaceSweep sweep : surfaceSweeps) {
|
||||
rasterizeSurfaceSweep(columns, sweep, false);
|
||||
SurfaceFootprint surface = surfaceFootprint(course);
|
||||
for (SurfaceLayerColumn column : surface.columns()) {
|
||||
addLayer(columns, column.x(), column.z(), column.terrain(), column.layer());
|
||||
}
|
||||
materializeSurfaceBankTerrain(columns);
|
||||
containSurfaceBanks(columns);
|
||||
SurfaceRasterIndex courseSurface = new SurfaceRasterIndex(List.of(surface), new Long2ObjectOpenHashMap<>());
|
||||
for (int segmentIndex = 0; segmentIndex < course.segments().size(); segmentIndex++) {
|
||||
HydraulicSegment segment = course.segments().get(segmentIndex);
|
||||
if (segment.type().isSurface()) {
|
||||
if (SurfaceFootprintCompiler.exposedSegment(segment)) {
|
||||
continue;
|
||||
}
|
||||
boolean clipStart = segmentIndex > 0
|
||||
@@ -3011,7 +3028,7 @@ final class HydrologyFootprintCompiler {
|
||||
private record ValidationCourseRaster(
|
||||
long courseId,
|
||||
List<HydrologyColumnSample> columns,
|
||||
List<SurfaceSweep> surfaceSweeps
|
||||
SurfaceFootprint surface
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -3228,17 +3245,23 @@ final class HydrologyFootprintCompiler {
|
||||
}
|
||||
|
||||
private final class SurfaceRasterIndex implements HydrologyCaveVoxelViewFactory.PlannedSurface {
|
||||
private final Long2ObjectOpenHashMap<ArrayList<SurfaceSweepEdge>> edgesByChunk;
|
||||
private final Long2ObjectOpenHashMap<HydrologyColumnSample> rawSurfaceColumns;
|
||||
private final Long2ObjectOpenHashMap<HydrologyColumnSample> surfaceColumns;
|
||||
private final Long2IntOpenHashMap validationNaturalHeights;
|
||||
private final Long2IntOpenHashMap resolvedHeights;
|
||||
|
||||
private SurfaceRasterIndex(
|
||||
List<SurfaceSweep> surfaceSweeps,
|
||||
List<SurfaceFootprint> footprints,
|
||||
Long2ObjectOpenHashMap<HydrologyColumnSample> validationSamples
|
||||
) {
|
||||
this.edgesByChunk = new Long2ObjectOpenHashMap<>();
|
||||
this.rawSurfaceColumns = new Long2ObjectOpenHashMap<>();
|
||||
Long2ObjectLinkedOpenHashMap<MutableColumn> merged = new Long2ObjectLinkedOpenHashMap<>();
|
||||
for (SurfaceFootprint footprint : footprints) {
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
addLayer(merged, column.x(), column.z(), column.terrain(), column.layer());
|
||||
}
|
||||
}
|
||||
this.surfaceColumns = new Long2ObjectOpenHashMap<>(merged.size());
|
||||
for (Long2ObjectMap.Entry<MutableColumn> entry : merged.long2ObjectEntrySet()) {
|
||||
surfaceColumns.put(entry.getLongKey(), entry.getValue().build());
|
||||
}
|
||||
this.validationNaturalHeights = new Long2IntOpenHashMap(validationSamples.size());
|
||||
for (HydrologyColumnSample sample : validationSamples.values()) {
|
||||
validationNaturalHeights.put(
|
||||
@@ -3246,145 +3269,37 @@ final class HydrologyFootprintCompiler {
|
||||
sample.naturalHeight()
|
||||
);
|
||||
}
|
||||
this.resolvedHeights = new Long2IntOpenHashMap();
|
||||
for (SurfaceSweep sweep : surfaceSweeps) {
|
||||
index(sweep);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int resolve(int x, int z, int naturalHeight) {
|
||||
long packed = RiverFootprint.pack(x, z);
|
||||
if (resolvedHeights.containsKey(packed)) {
|
||||
return resolvedHeights.get(packed);
|
||||
HydrologyColumnSample sample = surfaceColumns.get(packed);
|
||||
if (sample != null) {
|
||||
return sample.terrainHeight();
|
||||
}
|
||||
|
||||
MutableColumn column = null;
|
||||
int rasterNaturalHeight = naturalHeight;
|
||||
boolean rasterPresent = false;
|
||||
for (SurfaceProjection projection : projectionsAt(x, z)) {
|
||||
SurfaceCell cell = surfaceCell(projection, x, z);
|
||||
if (cell == null) {
|
||||
continue;
|
||||
}
|
||||
if (!rasterPresent) {
|
||||
rasterNaturalHeight = cell.terrain().naturalHeight();
|
||||
rasterPresent = true;
|
||||
}
|
||||
if (cell.layer() == null) {
|
||||
continue;
|
||||
}
|
||||
if (column == null) {
|
||||
column = new MutableColumn(x, z, cell.terrain(), settings.seaLevel());
|
||||
}
|
||||
column.add(cell.layer());
|
||||
if (validationNaturalHeights.containsKey(packed)) {
|
||||
return validationNaturalHeights.get(packed);
|
||||
}
|
||||
|
||||
int resolved;
|
||||
if (column != null) {
|
||||
resolved = containedSurfaceBankHeight(column.build());
|
||||
} else if (rasterPresent) {
|
||||
resolved = rasterNaturalHeight;
|
||||
} else if (validationNaturalHeights.containsKey(packed)) {
|
||||
resolved = validationNaturalHeights.get(packed);
|
||||
} else {
|
||||
return naturalHeight;
|
||||
}
|
||||
resolvedHeights.put(packed, resolved);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private int containedSurfaceBankHeight(HydrologyColumnSample sample) {
|
||||
return containedSurfaceBankSample(sample).terrainHeight();
|
||||
}
|
||||
|
||||
private HydrologyColumnSample containedSurfaceBankSample(HydrologyColumnSample sample) {
|
||||
if (sample.primarySurfaceFluidLayer().isPresent()) {
|
||||
return sample;
|
||||
}
|
||||
int minimumBankHeight = Integer.MIN_VALUE;
|
||||
for (int[] offset : HORIZONTAL_NEIGHBORS) {
|
||||
int neighborX = sample.x() + offset[0];
|
||||
int neighborZ = sample.z() + offset[1];
|
||||
HydrologyTerrainSample terrain = sampleTerrainBasis(neighborX, neighborZ);
|
||||
if (terrain == null) {
|
||||
continue;
|
||||
}
|
||||
HydrologyColumnSample neighbor = rawSurfaceColumnAt(
|
||||
neighborX,
|
||||
neighborZ,
|
||||
terrain.naturalHeight()
|
||||
);
|
||||
if (neighbor == null) {
|
||||
continue;
|
||||
}
|
||||
HydrologyColumnLayer fluid = neighbor.primarySurfaceFluidLayer().orElse(null);
|
||||
if (fluid != null && !fluid.oceanApron() && !fluid.fallingFluid()) {
|
||||
minimumBankHeight = Math.max(minimumBankHeight, Math.addExact(fluid.fluidHeadY(), 1));
|
||||
}
|
||||
}
|
||||
if (minimumBankHeight == Integer.MIN_VALUE) {
|
||||
return sample;
|
||||
}
|
||||
ArrayList<HydrologyColumnLayer> raised = new ArrayList<>(sample.layers().size());
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
raised.add(raisedDrySurfaceLayer(layer, sample.naturalHeight(), minimumBankHeight));
|
||||
}
|
||||
return new HydrologyColumnSample(
|
||||
sample.x(),
|
||||
sample.z(),
|
||||
sample.naturalHeight(),
|
||||
sample.seaLevel(),
|
||||
sample.ocean(),
|
||||
sample.parentBiomeKey(),
|
||||
raised
|
||||
);
|
||||
return naturalHeight;
|
||||
}
|
||||
|
||||
private HydrologyColumnSample surfaceColumnAt(int x, int z, int naturalHeight) {
|
||||
HydrologyColumnSample sample = rawSurfaceColumnAt(x, z, naturalHeight);
|
||||
return sample == null ? null : containedSurfaceBankSample(sample);
|
||||
}
|
||||
|
||||
private HydrologyColumnSample rawSurfaceColumnAt(int x, int z, int naturalHeight) {
|
||||
long packed = RiverFootprint.pack(x, z);
|
||||
HydrologyColumnSample cached = rawSurfaceColumns.get(packed);
|
||||
if (cached != null) {
|
||||
if (cached.naturalHeight() != naturalHeight) {
|
||||
throw new IllegalStateException("Hydrology surface and cave rasters disagree on natural terrain at "
|
||||
+ x + "," + z + ".");
|
||||
}
|
||||
return cached;
|
||||
}
|
||||
MutableColumn column = null;
|
||||
for (SurfaceProjection projection : projectionsAt(x, z)) {
|
||||
SurfaceCell cell = surfaceCell(projection, x, z);
|
||||
if (cell == null || cell.layer() == null) {
|
||||
continue;
|
||||
}
|
||||
if (column == null) {
|
||||
column = new MutableColumn(x, z, cell.terrain(), settings.seaLevel());
|
||||
}
|
||||
column.add(cell.layer());
|
||||
}
|
||||
if (column == null) {
|
||||
return null;
|
||||
}
|
||||
HydrologyColumnSample sample = column.build();
|
||||
if (sample.naturalHeight() != naturalHeight) {
|
||||
HydrologyColumnSample sample = surfaceColumns.get(RiverFootprint.pack(x, z));
|
||||
if (sample != null && sample.naturalHeight() != naturalHeight) {
|
||||
throw new IllegalStateException("Hydrology surface and cave rasters disagree on natural terrain at "
|
||||
+ x + "," + z + ".");
|
||||
}
|
||||
rawSurfaceColumns.put(packed, sample);
|
||||
return sample;
|
||||
}
|
||||
|
||||
private boolean ownsSurfaceChannelAt(int x, int z, long courseId) {
|
||||
for (SurfaceProjection projection : projectionsAt(x, z)) {
|
||||
SurfaceCell cell = surfaceCell(projection, x, z);
|
||||
HydrologyColumnLayer layer = cell == null ? null : cell.layer();
|
||||
if (layer != null
|
||||
&& layer.feature().courseId() == courseId
|
||||
HydrologyColumnSample sample = surfaceColumns.get(RiverFootprint.pack(x, z));
|
||||
if (sample == null) {
|
||||
return false;
|
||||
}
|
||||
for (HydrologyColumnLayer layer : sample.layers()) {
|
||||
if (layer.feature().courseId() == courseId
|
||||
&& layer.feature().type().isSurface()
|
||||
&& layer.channel()
|
||||
&& layer.terrainOwned()) {
|
||||
@@ -3393,67 +3308,6 @@ final class HydrologyFootprintCompiler {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<SurfaceProjection> projectionsAt(int x, int z) {
|
||||
ArrayList<SurfaceSweepEdge> edges = edgesByChunk.get(RiverFootprint.pack(x >> 4, z >> 4));
|
||||
if (edges == null) {
|
||||
return List.of();
|
||||
}
|
||||
IdentityHashMap<SurfaceSweep, SurfaceProjection> selected = new IdentityHashMap<>();
|
||||
for (SurfaceSweepEdge edge : edges) {
|
||||
SurfaceProjection candidate = surfaceProjection(edge.sweep(), edge.edgeIndex(), x, z);
|
||||
if (candidate == null) {
|
||||
continue;
|
||||
}
|
||||
SurfaceProjection current = selected.get(edge.sweep());
|
||||
if (current == null || prefersSurfaceProjection(candidate, current)) {
|
||||
selected.put(edge.sweep(), candidate);
|
||||
}
|
||||
}
|
||||
ArrayList<SurfaceProjection> projections = new ArrayList<>(selected.values());
|
||||
projections.sort(Comparator
|
||||
.comparingLong((SurfaceProjection projection) -> projection.sweep().course().id())
|
||||
.thenComparingInt((SurfaceProjection projection) -> projection.sweep().points().getFirst().point().x())
|
||||
.thenComparingInt((SurfaceProjection projection) -> projection.sweep().points().getFirst().point().z())
|
||||
.thenComparingInt(SurfaceProjection::edgeIndex));
|
||||
return List.copyOf(projections);
|
||||
}
|
||||
|
||||
private void index(SurfaceSweep sweep) {
|
||||
if (sweep.points().size() == 1) {
|
||||
index(new SurfaceSweepEdge(sweep, -1));
|
||||
return;
|
||||
}
|
||||
for (int edgeIndex = 0; edgeIndex < sweep.points().size() - 1; edgeIndex++) {
|
||||
index(new SurfaceSweepEdge(sweep, edgeIndex));
|
||||
}
|
||||
}
|
||||
|
||||
private void index(SurfaceSweepEdge edge) {
|
||||
SurfaceSweepPoint start = edge.sweep().points().get(Math.max(0, edge.edgeIndex()));
|
||||
SurfaceSweepPoint end = edge.edgeIndex() < 0
|
||||
? start
|
||||
: edge.sweep().points().get(edge.edgeIndex() + 1);
|
||||
int blockRadius = (int) StrictMath.ceil(Math.max(
|
||||
start.shape().totalRadius(),
|
||||
end.shape().totalRadius()
|
||||
));
|
||||
int minimumChunkX = (Math.min(start.point().x(), end.point().x()) - blockRadius) >> 4;
|
||||
int maximumChunkX = (Math.max(start.point().x(), end.point().x()) + blockRadius) >> 4;
|
||||
int minimumChunkZ = (Math.min(start.point().z(), end.point().z()) - blockRadius) >> 4;
|
||||
int maximumChunkZ = (Math.max(start.point().z(), end.point().z()) + blockRadius) >> 4;
|
||||
for (int chunkZ = minimumChunkZ; chunkZ <= maximumChunkZ; chunkZ++) {
|
||||
for (int chunkX = minimumChunkX; chunkX <= maximumChunkX; chunkX++) {
|
||||
long chunkKey = RiverFootprint.pack(chunkX, chunkZ);
|
||||
ArrayList<SurfaceSweepEdge> edges = edgesByChunk.get(chunkKey);
|
||||
if (edges == null) {
|
||||
edges = new ArrayList<>();
|
||||
edgesByChunk.put(chunkKey, edges);
|
||||
}
|
||||
edges.add(edge);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MutableColumn {
|
||||
|
||||
@@ -5,6 +5,9 @@ import art.arcane.iris.engine.hydrology.cave.CaveVoxelView;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveCandidate;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCaveContainmentPlanner;
|
||||
import art.arcane.iris.engine.hydrology.cave.HydrologyCavePlan;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseBuilder;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceCourseResult;
|
||||
import art.arcane.iris.engine.hydrology.surface.SurfaceTerminal;
|
||||
import art.arcane.iris.spi.IrisPlatforms;
|
||||
import art.arcane.iris.util.common.parallel.MultiBurst;
|
||||
import art.arcane.iris.util.project.noise.SimplexNoise;
|
||||
@@ -116,6 +119,7 @@ public final class HydrologyPlanner {
|
||||
private final SimplexNoise routeAnchorZ;
|
||||
private final SimplexNoise routeWormPrimary;
|
||||
private final SimplexNoise routeWormDetail;
|
||||
private final SurfaceCourseBuilder surfaceCourseBuilder;
|
||||
|
||||
public HydrologyPlanner(long worldSeed, HydrologyPlannerSettings settings, HydrologyTerrainSampler sampler) {
|
||||
this(
|
||||
@@ -230,6 +234,13 @@ public final class HydrologyPlanner {
|
||||
this.routeAnchorZ = new SimplexNoise(HydrologyHash.mix(worldSeed, ROUTE_ANCHOR_Z_SALT));
|
||||
this.routeWormPrimary = new SimplexNoise(HydrologyHash.mix(worldSeed, ROUTE_WORM_PRIMARY_SALT));
|
||||
this.routeWormDetail = new SimplexNoise(HydrologyHash.mix(worldSeed, ROUTE_WORM_DETAIL_SALT));
|
||||
this.surfaceCourseBuilder = new SurfaceCourseBuilder(
|
||||
settings.surface(),
|
||||
this::sampleBasis,
|
||||
geometrySampler,
|
||||
settings.seaLevel(),
|
||||
settings.routing().branching().minimumSurfaceCourseLength()
|
||||
);
|
||||
}
|
||||
|
||||
public long worldSeed() {
|
||||
@@ -421,7 +432,7 @@ public final class HydrologyPlanner {
|
||||
boolean fallbackSelected = false;
|
||||
while (fallbackOutletIndex < fallbackOutlets.size()) {
|
||||
OutletCandidate fallbackOutlet = fallbackOutlets.get(fallbackOutletIndex++);
|
||||
for (int routeMode = 0; routeMode < 2; routeMode++) {
|
||||
for (int routeMode = 0; routeMode < 1; routeMode++) {
|
||||
RoutingPlan fallbackRouting = routeMode == 0
|
||||
? requireOrganicSurface(buildRouting(grid, List.of(fallbackOutlet)))
|
||||
: buildContainedSurfaceRouting(grid, List.of(fallbackOutlet));
|
||||
@@ -538,17 +549,6 @@ public final class HydrologyPlanner {
|
||||
int maximumPasses = Math.addExact(initial.courses().size(), 1);
|
||||
for (int pass = 0; pass < maximumPasses; pass++) {
|
||||
RiverFootprint footprint = footprintCompiler.compile(current.courses());
|
||||
HydrologyCaveCourseFilter.Result incisionContained = rejectMaterializedSurfaceIncision(
|
||||
current,
|
||||
footprint,
|
||||
diagnostics,
|
||||
footprintCompiler
|
||||
);
|
||||
if (!incisionContained.courses().equals(current.courses())) {
|
||||
current = incisionContained;
|
||||
revalidationRequired = true;
|
||||
continue;
|
||||
}
|
||||
if (current.cavePlans().isEmpty()) {
|
||||
return new MaterializedHydrology(current, footprint);
|
||||
}
|
||||
@@ -596,58 +596,6 @@ public final class HydrologyPlanner {
|
||||
throw new IllegalStateException("Final hydrology cave publication exceeded its course bound.");
|
||||
}
|
||||
|
||||
private HydrologyCaveCourseFilter.Result rejectMaterializedSurfaceIncision(
|
||||
HydrologyCaveCourseFilter.Result current,
|
||||
RiverFootprint footprint,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics,
|
||||
HydrologyFootprintCompiler footprintCompiler
|
||||
) {
|
||||
ArrayList<RiverCourse> retained = new ArrayList<>(current.courses().size());
|
||||
for (RiverCourse course : current.courses()) {
|
||||
if (footprintCompiler.surfaceIncisionContained(course, footprint)) {
|
||||
retained.add(course);
|
||||
continue;
|
||||
}
|
||||
HydrologyPoint source = course.segments().getFirst().centerline().getFirst();
|
||||
HydrologyCandidateRejection rejection = HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED;
|
||||
diagnostics.add(new HydrologyDiagnosticCandidate(
|
||||
HydrologyHash.mix(course.id(), DIAGNOSTIC_SALT, rejection.ordinal()),
|
||||
HydrologyCandidateKind.SOURCE,
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
source,
|
||||
rejection
|
||||
));
|
||||
}
|
||||
if (retained.size() == current.courses().size()) {
|
||||
return current;
|
||||
}
|
||||
HydrologyFootprintCompiler.ValidationRaster validation = footprintCompiler.compileValidation(retained);
|
||||
HydrologyObservedPlannedSurface plannedSurface = new HydrologyObservedPlannedSurface(
|
||||
validation.plannedSurface()
|
||||
);
|
||||
CaveVoxelView caveView = Objects.requireNonNull(
|
||||
caveViewFactory.create(plannedSurface),
|
||||
"Hydrology cave view factory returned null"
|
||||
);
|
||||
return new HydrologyCaveCourseFilter(
|
||||
caveView,
|
||||
new HydrologyCaveCourseFilter.Options(
|
||||
settings.underground().connectToExistingCaves(),
|
||||
settings.outlets().coastalGrotto().maximumVolume(),
|
||||
settings.outlets().inlandGrotto().maximumVolume()
|
||||
),
|
||||
null,
|
||||
null,
|
||||
plannedSurface
|
||||
).filter(
|
||||
current.nodes(),
|
||||
current.edges(),
|
||||
current.outlets(),
|
||||
retained,
|
||||
validation,
|
||||
diagnostics
|
||||
);
|
||||
}
|
||||
|
||||
private boolean compactSurfaceMatchesMaterializedPlans(
|
||||
HydrologyCaveCourseFilter.Result result,
|
||||
@@ -1161,7 +1109,6 @@ public final class HydrologyPlanner {
|
||||
List<RiverCourse> normalizedOutletCourses = normalizeOutletContinuations(normalizedTrunkCourses);
|
||||
courses.clear();
|
||||
courses.addAll(normalizedOutletCourses);
|
||||
rejectUncontainedSurfaceCourses(courses, footprintCompiler, diagnostics);
|
||||
if (includeDeepFluids) {
|
||||
compileDeepFluidCourses(key, courses, diagnostics);
|
||||
}
|
||||
@@ -1202,44 +1149,6 @@ public final class HydrologyPlanner {
|
||||
return new PublicationAttempt(containment, diagnostics);
|
||||
}
|
||||
|
||||
private void rejectUncontainedSurfaceCourses(
|
||||
List<RiverCourse> courses,
|
||||
HydrologyFootprintCompiler footprintCompiler,
|
||||
List<HydrologyDiagnosticCandidate> diagnostics
|
||||
) {
|
||||
int courseIndex = 0;
|
||||
while (courseIndex < courses.size()) {
|
||||
RiverCourse course = courses.get(courseIndex);
|
||||
if (course.type() != RiverCourseType.SURFACE) {
|
||||
courseIndex++;
|
||||
continue;
|
||||
}
|
||||
HydrologyCandidateRejection rejection;
|
||||
if (!footprintCompiler.surfaceIncisionContained(course)) {
|
||||
rejection = HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED;
|
||||
} else if (!footprintCompiler.surfaceHeadwaterRampSupported(course)) {
|
||||
rejection = HydrologyCandidateRejection.SURFACE_EXPOSURE;
|
||||
} else if (!footprintCompiler.surfaceBanksContained(course)) {
|
||||
rejection = HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED;
|
||||
} else {
|
||||
courseIndex++;
|
||||
continue;
|
||||
}
|
||||
HydrologyPoint source = course.segments().getFirst().centerline().getFirst();
|
||||
diagnostics.add(new HydrologyDiagnosticCandidate(
|
||||
HydrologyHash.mix(
|
||||
course.id(),
|
||||
DIAGNOSTIC_SALT,
|
||||
rejection.ordinal()
|
||||
),
|
||||
HydrologyCandidateKind.SOURCE,
|
||||
HydrologyFeatureType.SURFACE_POOL,
|
||||
source,
|
||||
rejection
|
||||
));
|
||||
courses.remove(courseIndex);
|
||||
}
|
||||
}
|
||||
|
||||
private SourceCompilation compileSourceSelection(
|
||||
boolean surface,
|
||||
@@ -4472,7 +4381,7 @@ public final class HydrologyPlanner {
|
||||
continue;
|
||||
}
|
||||
long courseId = HydrologyHash.mix(sourceCourseId, path.outlet().id());
|
||||
HydrologyPoint trunkPoint = path.points().getLast();
|
||||
HydrologyPoint trunkPoint = path.points().getFirst();
|
||||
HydrologyTerrainSample trunkTerrain = Objects.requireNonNull(
|
||||
sampleDetailed(trunkPoint.x(), trunkPoint.z()),
|
||||
"Hydrology surface trunk left sampled terrain"
|
||||
@@ -4535,183 +4444,53 @@ public final class HydrologyPlanner {
|
||||
GridNode source,
|
||||
CoursePath path
|
||||
) {
|
||||
int pairCount = path.points().size() - 1;
|
||||
if (pairCount < 1) {
|
||||
if (path.points().size() < 2) {
|
||||
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.COURSE_TOO_SHORT);
|
||||
}
|
||||
int[] widths = new int[pairCount];
|
||||
int[] depths = new int[pairCount];
|
||||
for (int index = 0; index < pairCount; index++) {
|
||||
HydrologyPoint point = path.points().get(index);
|
||||
HydrologyTerrainSample terrain = sampleBasis(point.x(), point.z());
|
||||
int discharge = Math.max(1, path.pairEdges().get(index).contributingSurfaceSources());
|
||||
widths[index] = scaledDimension(
|
||||
sampleGeometry(
|
||||
HydrologyGeometrySampler.Field.SURFACE_WIDTH,
|
||||
profileKey,
|
||||
point.x(),
|
||||
point.z(),
|
||||
0L,
|
||||
settings.surface().minimumWidth(),
|
||||
settings.surface().maximumWidth()
|
||||
),
|
||||
settings.surface().minimumWidth(),
|
||||
settings.surface().maximumWidth(),
|
||||
discharge,
|
||||
terrain.widthMultiplier()
|
||||
);
|
||||
depths[index] = scaledDimension(
|
||||
sampleGeometry(
|
||||
HydrologyGeometrySampler.Field.SURFACE_DEPTH,
|
||||
profileKey,
|
||||
point.x(),
|
||||
point.z(),
|
||||
0L,
|
||||
settings.surface().minimumDepth(),
|
||||
settings.surface().maximumDepth()
|
||||
),
|
||||
settings.surface().minimumDepth(),
|
||||
settings.surface().maximumDepth(),
|
||||
discharge,
|
||||
terrain.depthMultiplier()
|
||||
);
|
||||
RiverOutlet outlet = path.outlet();
|
||||
boolean surfaceSinkhole = path.reachesOutlet() && surfaceSinkhole(outlet);
|
||||
SurfaceTerminal terminal;
|
||||
if (!path.reachesOutlet() || outlet.type() == HydrologyFeatureType.INLAND_GROTTO) {
|
||||
terminal = SurfaceTerminal.SINKHOLE;
|
||||
} else if (outlet.type() == HydrologyFeatureType.COASTAL_GROTTO) {
|
||||
terminal = SurfaceTerminal.COASTAL_GROTTO;
|
||||
} else {
|
||||
terminal = SurfaceTerminal.OCEAN_MOUTH;
|
||||
}
|
||||
int outletHead = outletHead(path.outlet());
|
||||
boolean surfaceSinkhole = path.reachesOutlet() && surfaceSinkhole(path.outlet());
|
||||
int minimumPathHead = surfaceSinkhole ? Math.addExact(outletHead, 1) : outletHead;
|
||||
long hydraulicStableId = path.outlet().id();
|
||||
int[] maximumHeads = surfaceMaximumHeads(hydraulicStableId, profileKey, path, widths);
|
||||
int[] heads = new int[path.points().size()];
|
||||
if (maximumHeads[0] < minimumPathHead) {
|
||||
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_HEAD_RANGE);
|
||||
}
|
||||
heads[0] = maximumHeads[0];
|
||||
for (int index = 1; index < path.points().size(); index++) {
|
||||
if (maximumHeads[index] < minimumPathHead) {
|
||||
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_HEAD_RANGE);
|
||||
}
|
||||
heads[index] = Math.min(heads[index - 1], maximumHeads[index]);
|
||||
}
|
||||
int targetPoolLength = targetPoolLength(hydraulicStableId, profileKey, path);
|
||||
poolHeads(path, heads, maximumHeads, targetPoolLength);
|
||||
if (!distributeSurfaceHeadLoss(path, heads, maximumHeads, minimumPathHead)) {
|
||||
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_HEAD_DISTRIBUTION);
|
||||
}
|
||||
if (!ridgeLengthAccepted(path, heads)) {
|
||||
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_RIDGE_TUNNEL_LIMIT);
|
||||
}
|
||||
if (!surfaceHeadLossSupportedByTerrain(path, heads, widths, depths)) {
|
||||
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_DROP_UNSUPPORTED);
|
||||
}
|
||||
if (!surfaceCorridorSupported(path, heads, widths)) {
|
||||
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_CORRIDOR_UNSUPPORTED);
|
||||
}
|
||||
ArrayList<HydraulicSegment> segments = new ArrayList<>(pairCount + 1);
|
||||
int index = 0;
|
||||
while (index < pairCount) {
|
||||
int upstreamHead = heads[index];
|
||||
int downstreamHead = heads[index + 1];
|
||||
HydrologyFeatureType pairType = surfaceSegmentType(
|
||||
path,
|
||||
index,
|
||||
upstreamHead,
|
||||
downstreamHead,
|
||||
widths[index],
|
||||
depths[index]
|
||||
);
|
||||
if (!isDirectMouthPair(path, index)
|
||||
&& addMixedSurfaceBoreSegments(
|
||||
courseId,
|
||||
index,
|
||||
upstreamHead,
|
||||
downstreamHead,
|
||||
widths[index],
|
||||
depths[index],
|
||||
path.points().get(index),
|
||||
path.points().get(index + 1),
|
||||
segments
|
||||
)) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
if (upstreamHead > downstreamHead
|
||||
&& pairType.isSurface()
|
||||
&& !isDirectMouthPair(path, index)) {
|
||||
int runEnd = surfaceTransitionRunEnd(path, heads, widths, depths, index, targetPoolLength);
|
||||
boolean added = addSurfaceTransitionRunSegments(
|
||||
courseId,
|
||||
index,
|
||||
runEnd,
|
||||
averageDimension(widths, index, runEnd),
|
||||
averageDimension(depths, index, runEnd),
|
||||
path,
|
||||
heads,
|
||||
segments
|
||||
);
|
||||
if (!added) {
|
||||
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_TRANSITION_UNSUPPORTED);
|
||||
}
|
||||
index = runEnd + 1;
|
||||
continue;
|
||||
}
|
||||
List<HydrologyPoint> centerline = List.of(
|
||||
withY(path.points().get(index), upstreamHead),
|
||||
withY(path.points().get(index + 1), downstreamHead)
|
||||
);
|
||||
boolean added = pairType.isSurface()
|
||||
&& isDirectMouthPair(path, index)
|
||||
&& downstreamHead < upstreamHead
|
||||
? addCoastalMouthDropSegments(
|
||||
courseId,
|
||||
index,
|
||||
upstreamHead,
|
||||
downstreamHead,
|
||||
widths[index],
|
||||
depths[index],
|
||||
centerline,
|
||||
true,
|
||||
segments
|
||||
)
|
||||
: addHydraulicSegments(
|
||||
courseId,
|
||||
index,
|
||||
pairType,
|
||||
upstreamHead,
|
||||
downstreamHead,
|
||||
widths[index],
|
||||
depths[index],
|
||||
centerline,
|
||||
true,
|
||||
segments
|
||||
);
|
||||
if (!added) {
|
||||
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_SEGMENT_UNSUPPORTED);
|
||||
}
|
||||
index++;
|
||||
}
|
||||
if (surfaceSinkhole && heads[heads.length - 1] <= outletHead) {
|
||||
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_SINKHOLE_CLEARANCE);
|
||||
int outletHead = outletHead(outlet);
|
||||
int terminalHead = surfaceSinkhole ? Math.addExact(outletHead, 1) : outletHead;
|
||||
SurfaceCourseResult result = surfaceCourseBuilder.build(
|
||||
worldSeed,
|
||||
courseId,
|
||||
profileKey,
|
||||
path.points(),
|
||||
terminal,
|
||||
terminalHead
|
||||
);
|
||||
if (!result.accepted()) {
|
||||
return SurfaceCourseBuild.rejected(result.rejection());
|
||||
}
|
||||
ArrayList<HydraulicSegment> segments = new ArrayList<>(result.segments());
|
||||
if (path.reachesOutlet()) {
|
||||
CoursePath terminalPath = new CoursePath(
|
||||
List.of(result.pathEnd()),
|
||||
path.pairEdges(),
|
||||
path.edges(),
|
||||
outlet,
|
||||
true,
|
||||
false,
|
||||
path.organicSurfaceRequired()
|
||||
);
|
||||
appendOutletSegments(
|
||||
RiverCourseType.SURFACE,
|
||||
courseId,
|
||||
path,
|
||||
heads[heads.length - 1],
|
||||
widths,
|
||||
depths,
|
||||
terminalPath,
|
||||
result.lastHead(),
|
||||
new int[] {result.lastWidth()},
|
||||
new int[] {result.lastDepth()},
|
||||
segments
|
||||
);
|
||||
}
|
||||
containUnsupportedSurfaceSegments(segments);
|
||||
containBriefSurfaceReopenings(segments);
|
||||
smoothFlatSurfaceDimensions(segments);
|
||||
if (!surfaceExposureAccepted(segments)) {
|
||||
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_EXPOSURE);
|
||||
}
|
||||
if (path.containedSurface() && !surfaceTransitionRecoveriesAccepted(segments)) {
|
||||
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_TRANSITION_UNSUPPORTED);
|
||||
}
|
||||
if (path.organicSurfaceRequired() && !surfaceShapeAccepted(path.points())) {
|
||||
return SurfaceCourseBuild.rejected(HydrologyCandidateRejection.SURFACE_SHAPE_UNSUPPORTED);
|
||||
}
|
||||
@@ -4720,7 +4499,7 @@ public final class HydrologyPlanner {
|
||||
courseId,
|
||||
RiverCourseType.SURFACE,
|
||||
OptionalLong.of(source.id()),
|
||||
OptionalLong.of(path.outlet().id()),
|
||||
OptionalLong.of(outlet.id()),
|
||||
profileKey,
|
||||
discharge,
|
||||
path.edges(),
|
||||
|
||||
+7
-9
@@ -2,11 +2,9 @@ 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 {
|
||||
@@ -30,7 +28,6 @@ public final class ChannelProfileBuilder {
|
||||
|
||||
public ChannelProfile build(
|
||||
SurfaceCenterline centerline,
|
||||
List<HydrologyPoint> path,
|
||||
String profileKey,
|
||||
boolean directOcean
|
||||
) {
|
||||
@@ -39,12 +36,13 @@ public final class ChannelProfileBuilder {
|
||||
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 stationX = centerline.x()[station];
|
||||
int stationZ = centerline.z()[station];
|
||||
int sampledWidth = geometry.sample(
|
||||
HydrologyGeometrySampler.Field.SURFACE_WIDTH,
|
||||
profileKey,
|
||||
point.x(),
|
||||
point.z(),
|
||||
stationX,
|
||||
stationZ,
|
||||
0L,
|
||||
surface.minimumWidth(),
|
||||
surface.maximumWidth()
|
||||
@@ -52,13 +50,13 @@ public final class ChannelProfileBuilder {
|
||||
int sampledDepth = geometry.sample(
|
||||
HydrologyGeometrySampler.Field.SURFACE_DEPTH,
|
||||
profileKey,
|
||||
point.x(),
|
||||
point.z(),
|
||||
stationX,
|
||||
stationZ,
|
||||
0L,
|
||||
surface.minimumDepth(),
|
||||
surface.maximumDepth()
|
||||
);
|
||||
HydrologyTerrainSample terrain = sampler.sample(centerline.x()[station], centerline.z()[station]);
|
||||
HydrologyTerrainSample terrain = sampler.sample(stationX, stationZ);
|
||||
double widthMultiplier = terrain == null ? 1D : terrain.widthMultiplier();
|
||||
double depthMultiplier = terrain == null ? 1D : terrain.depthMultiplier();
|
||||
bank[station] = terrain == null ? 1D : terrain.bankMultiplier();
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydraulicSegment;
|
||||
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.HydrologyTerrainSampler;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SurfaceCourseBuilder {
|
||||
private final HydrologyPlannerSettings.Surface surface;
|
||||
private final HydrologyTerrainSampler sampler;
|
||||
private final HydrologyGeometrySampler geometry;
|
||||
private final int seaLevel;
|
||||
private final int minimumCourseLength;
|
||||
|
||||
public SurfaceCourseBuilder(
|
||||
HydrologyPlannerSettings.Surface surface,
|
||||
HydrologyTerrainSampler sampler,
|
||||
HydrologyGeometrySampler geometry,
|
||||
int seaLevel,
|
||||
int minimumCourseLength
|
||||
) {
|
||||
this.surface = Objects.requireNonNull(surface, "surface");
|
||||
this.sampler = Objects.requireNonNull(sampler, "sampler");
|
||||
this.geometry = Objects.requireNonNull(geometry, "geometry");
|
||||
this.seaLevel = seaLevel;
|
||||
this.minimumCourseLength = minimumCourseLength;
|
||||
}
|
||||
|
||||
public SurfaceCourseResult build(
|
||||
long worldSeed,
|
||||
long courseId,
|
||||
String profileKey,
|
||||
List<HydrologyPoint> path,
|
||||
SurfaceTerminal terminal,
|
||||
int terminalHead
|
||||
) {
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(path);
|
||||
boolean directOcean = terminal == SurfaceTerminal.OCEAN_MOUTH;
|
||||
ChannelProfile channel = new ChannelProfileBuilder(surface, sampler, geometry)
|
||||
.build(centerline, profileKey, directOcean);
|
||||
ValleyProfile valley = new ValleyProfileSolver(surface, sampler, seaLevel, minimumCourseLength)
|
||||
.solve(centerline, channel, terminal, terminalHead);
|
||||
if (!valley.accepted()) {
|
||||
return SurfaceCourseResult.rejected(valley.rejection());
|
||||
}
|
||||
int exposed = valley.exposedStations();
|
||||
boolean coastalDrop = directOcean && valley.head()[exposed - 1] > seaLevel;
|
||||
int stations = coastalDrop ? exposed + 1 : exposed;
|
||||
int[] x = new int[stations];
|
||||
int[] z = new int[stations];
|
||||
int[] head = new int[stations];
|
||||
double[] width = new double[stations];
|
||||
double[] depth = new double[stations];
|
||||
for (int station = 0; station < exposed; station++) {
|
||||
x[station] = centerline.x()[station];
|
||||
z[station] = centerline.z()[station];
|
||||
head[station] = valley.head()[station];
|
||||
width[station] = channel.halfWidth()[station] * 2D;
|
||||
depth[station] = channel.depth()[station];
|
||||
}
|
||||
if (coastalDrop) {
|
||||
int last = exposed - 1;
|
||||
if (exposed < centerline.size()) {
|
||||
x[exposed] = centerline.x()[exposed];
|
||||
z[exposed] = centerline.z()[exposed];
|
||||
} else {
|
||||
x[exposed] = x[last] + (int) StrictMath.round(centerline.tangentX()[last]);
|
||||
z[exposed] = z[last] + (int) StrictMath.round(centerline.tangentZ()[last]);
|
||||
if (x[exposed] == x[last] && z[exposed] == z[last]) {
|
||||
x[exposed] = x[last] + 1;
|
||||
}
|
||||
}
|
||||
head[exposed] = seaLevel;
|
||||
width[exposed] = width[last];
|
||||
depth[exposed] = depth[last];
|
||||
}
|
||||
List<HydraulicSegment> segments = SurfaceSegmentLabeler.label(
|
||||
worldSeed, courseId, x, z, head, width, depth, surface.banks());
|
||||
if (segments.isEmpty()) {
|
||||
return SurfaceCourseResult.rejected(HydrologyCandidateRejection.COURSE_TOO_SHORT);
|
||||
}
|
||||
HydraulicSegment last = segments.getLast();
|
||||
int lastStation = stations - 1;
|
||||
return new SurfaceCourseResult(
|
||||
segments,
|
||||
head[lastStation],
|
||||
last.width(),
|
||||
last.depth(),
|
||||
new HydrologyPoint(x[lastStation], head[lastStation], z[lastStation]),
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydraulicSegment;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyCandidateRejection;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyPoint;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record SurfaceCourseResult(
|
||||
List<HydraulicSegment> segments,
|
||||
int lastHead,
|
||||
int lastWidth,
|
||||
int lastDepth,
|
||||
HydrologyPoint pathEnd,
|
||||
HydrologyCandidateRejection rejection
|
||||
) {
|
||||
public SurfaceCourseResult {
|
||||
segments = List.copyOf(Objects.requireNonNull(segments, "segments"));
|
||||
}
|
||||
|
||||
public boolean accepted() {
|
||||
return rejection == null;
|
||||
}
|
||||
|
||||
public static SurfaceCourseResult rejected(HydrologyCandidateRejection rejection) {
|
||||
return new SurfaceCourseResult(List.of(), 0, 0, 0, null, Objects.requireNonNull(rejection, "rejection"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydraulicSegment;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyHash;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public final class SurfaceFeatureRefs {
|
||||
private static final int ROLE_CHANNEL = 1;
|
||||
private static final int ROLE_SHORE = 2;
|
||||
private static final int ROLE_GRADING = 3;
|
||||
private static final int ROLE_SOURCE = 6;
|
||||
|
||||
private final long courseId;
|
||||
private final HashMap<Key, HydrologyFeatureRef> features;
|
||||
|
||||
public SurfaceFeatureRefs(long courseId) {
|
||||
this.courseId = courseId;
|
||||
this.features = new HashMap<>();
|
||||
}
|
||||
|
||||
public HydrologyFeatureRef feature(
|
||||
HydraulicSegment segment,
|
||||
SurfaceRole role,
|
||||
boolean source,
|
||||
int x,
|
||||
int y,
|
||||
int z,
|
||||
int flowX,
|
||||
int flowZ
|
||||
) {
|
||||
int roleCode = source ? ROLE_SOURCE : switch (role) {
|
||||
case CHANNEL -> ROLE_CHANNEL;
|
||||
case SHORE -> ROLE_SHORE;
|
||||
case BANK -> ROLE_GRADING;
|
||||
};
|
||||
int featureFlowX = Integer.compare(flowX, 0);
|
||||
int featureFlowZ = Integer.compare(flowZ, 0);
|
||||
Key key = new Key(segment.id(), segment.type(), roleCode, featureFlowX, featureFlowZ);
|
||||
HydrologyFeatureRef existing = features.get(key);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
HydrologyFeatureRef created = new HydrologyFeatureRef(
|
||||
HydrologyHash.mix(courseId, segment.id(), segment.type().ordinal(), roleCode, featureFlowX, featureFlowZ),
|
||||
segment.type(),
|
||||
courseId,
|
||||
segment.id(),
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
featureFlowX,
|
||||
featureFlowZ,
|
||||
source
|
||||
);
|
||||
features.put(key, created);
|
||||
return created;
|
||||
}
|
||||
|
||||
private record Key(long segmentId, HydrologyFeatureType type, int role, int flowX, int flowZ) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public record SurfaceFootprint(List<SurfaceLayerColumn> columns, int uncontainedWetCells) {
|
||||
public SurfaceFootprint {
|
||||
columns = List.copyOf(Objects.requireNonNull(columns, "columns"));
|
||||
}
|
||||
|
||||
public static SurfaceFootprint empty() {
|
||||
return new SurfaceFootprint(List.of(), 0);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return columns.isEmpty();
|
||||
}
|
||||
}
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydraulicSegment;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureRef;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyGeometrySampler;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyHash;
|
||||
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.RiverCourse;
|
||||
import art.arcane.iris.engine.hydrology.RiverCourseType;
|
||||
import art.arcane.iris.engine.hydrology.RiverFootprint;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SurfaceFootprintCompiler {
|
||||
private static final long COURSE_SEED_SALT = 0x535552464143455fL;
|
||||
|
||||
private final HydrologyPlannerSettings settings;
|
||||
private final HydrologyTerrainSampler sampler;
|
||||
private final HydrologyGeometrySampler geometry;
|
||||
|
||||
public SurfaceFootprintCompiler(
|
||||
HydrologyPlannerSettings settings,
|
||||
HydrologyTerrainSampler sampler,
|
||||
HydrologyGeometrySampler geometry
|
||||
) {
|
||||
this.settings = Objects.requireNonNull(settings, "settings");
|
||||
this.sampler = Objects.requireNonNull(sampler, "sampler");
|
||||
this.geometry = Objects.requireNonNull(geometry, "geometry");
|
||||
}
|
||||
|
||||
public static boolean exposedSegment(HydraulicSegment segment) {
|
||||
return segment.type().isSurface() && !segment.fallingFluid();
|
||||
}
|
||||
|
||||
public SurfaceFootprint compile(RiverCourse course) {
|
||||
if (course.type() != RiverCourseType.SURFACE) {
|
||||
return SurfaceFootprint.empty();
|
||||
}
|
||||
ArrayList<HydraulicSegment> exposed = new ArrayList<>();
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
if (!exposedSegment(segment)) {
|
||||
break;
|
||||
}
|
||||
exposed.add(segment);
|
||||
}
|
||||
if (exposed.isEmpty()) {
|
||||
return SurfaceFootprint.empty();
|
||||
}
|
||||
Stations stations = stations(exposed);
|
||||
if (stations.count() < 1) {
|
||||
return SurfaceFootprint.empty();
|
||||
}
|
||||
SurfaceTerminal terminal = terminal(course, exposed);
|
||||
SurfaceCenterline centerline = SurfaceCenterline.densify(stations.points());
|
||||
ChannelProfile channel = new ChannelProfileBuilder(settings.surface(), sampler, geometry)
|
||||
.build(centerline, course.profileKey(), terminal == SurfaceTerminal.OCEAN_MOUTH);
|
||||
ValleyProfile valley = ValleyProfile.fromHeads(stations.head(), stations.exposedStations());
|
||||
ErosionField field = new ErosionFieldCompiler(settings.surface(), sampler, settings.seaLevel()).compile(
|
||||
HydrologyHash.mix(course.id(), COURSE_SEED_SALT),
|
||||
centerline,
|
||||
channel,
|
||||
valley,
|
||||
terminal,
|
||||
settings.outlets().maximumOceanApron()
|
||||
);
|
||||
ArrayList<SurfaceColumn> ordered = new ArrayList<>(field.columns().values());
|
||||
ordered.sort(Comparator
|
||||
.comparingInt(SurfaceColumn::station)
|
||||
.thenComparingLong((SurfaceColumn column) -> RiverFootprint.pack(column.x(), column.z())));
|
||||
SurfaceFeatureRefs features = new SurfaceFeatureRefs(course.id());
|
||||
ArrayList<SurfaceLayerColumn> columns = new ArrayList<>(ordered.size());
|
||||
for (SurfaceColumn column : ordered) {
|
||||
HydraulicSegment segment = exposed.get(stations.segmentIndex()[column.station()]);
|
||||
int flowX = (int) StrictMath.round(centerline.tangentX()[column.station()]);
|
||||
int flowZ = (int) StrictMath.round(centerline.tangentZ()[column.station()]);
|
||||
boolean source = column.station() == 0 && column.role() == SurfaceRole.CHANNEL && !column.apron();
|
||||
int y = column.role() == SurfaceRole.CHANNEL ? column.headY() : column.height();
|
||||
HydrologyFeatureRef feature = features.feature(segment, column.role(), source, column.x(), y, column.z(), flowX, flowZ);
|
||||
columns.add(new SurfaceLayerColumn(
|
||||
column.x(),
|
||||
column.z(),
|
||||
column.terrain(),
|
||||
layer(feature, column, course.profileKey()),
|
||||
column.role(),
|
||||
column.apron()
|
||||
));
|
||||
}
|
||||
return new SurfaceFootprint(columns, field.uncontainedWetCells());
|
||||
}
|
||||
|
||||
private static HydrologyColumnLayer layer(HydrologyFeatureRef feature, SurfaceColumn column, String profileKey) {
|
||||
HydrologyTerrainSample terrain = column.terrain();
|
||||
if (column.apron()) {
|
||||
return new HydrologyColumnLayer(
|
||||
feature,
|
||||
column.headY(),
|
||||
column.headY(),
|
||||
column.headY(),
|
||||
true, false, false, true, false, false, false, false, true,
|
||||
profileKey,
|
||||
terrain.surfaceBiomeKey(),
|
||||
terrain.mouthBiomeKey(),
|
||||
terrain.shoreBiomeKey(),
|
||||
terrain.bankBiomeKey(),
|
||||
terrain.floodedCaveBiomeKey()
|
||||
);
|
||||
}
|
||||
boolean channel = column.role() == SurfaceRole.CHANNEL;
|
||||
boolean shore = column.role() == SurfaceRole.SHORE;
|
||||
return new HydrologyColumnLayer(
|
||||
feature,
|
||||
column.height(),
|
||||
column.headY(),
|
||||
column.headY(),
|
||||
channel,
|
||||
shore,
|
||||
!channel,
|
||||
channel,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
channel,
|
||||
false,
|
||||
profileKey,
|
||||
terrain.surfaceBiomeKey(),
|
||||
terrain.mouthBiomeKey(),
|
||||
terrain.shoreBiomeKey(),
|
||||
terrain.bankBiomeKey(),
|
||||
terrain.floodedCaveBiomeKey()
|
||||
);
|
||||
}
|
||||
|
||||
private static SurfaceTerminal terminal(RiverCourse course, List<HydraulicSegment> exposed) {
|
||||
if (exposed.getLast().type() == HydrologyFeatureType.MOUTH) {
|
||||
return SurfaceTerminal.OCEAN_MOUTH;
|
||||
}
|
||||
for (HydraulicSegment segment : course.segments()) {
|
||||
if (segment.type() == HydrologyFeatureType.MOUTH) {
|
||||
return SurfaceTerminal.OCEAN_MOUTH;
|
||||
}
|
||||
if (segment.type() == HydrologyFeatureType.COASTAL_GROTTO) {
|
||||
return SurfaceTerminal.COASTAL_GROTTO;
|
||||
}
|
||||
}
|
||||
return SurfaceTerminal.SINKHOLE;
|
||||
}
|
||||
|
||||
private static Stations stations(List<HydraulicSegment> exposed) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
IntArrayList segmentIndices = new IntArrayList();
|
||||
IntArrayList heads = new IntArrayList();
|
||||
int exposedStations = 0;
|
||||
for (int segmentIndex = 0; segmentIndex < exposed.size(); segmentIndex++) {
|
||||
HydraulicSegment segment = exposed.get(segmentIndex);
|
||||
boolean mouth = segment.type() == HydrologyFeatureType.MOUTH;
|
||||
List<HydrologyPoint> centerline = segment.centerline();
|
||||
for (int pointIndex = 0; pointIndex < centerline.size(); pointIndex++) {
|
||||
HydrologyPoint current = centerline.get(pointIndex);
|
||||
if (pointIndex == 0) {
|
||||
appendStation(points, segmentIndices, heads, current, segmentIndex);
|
||||
continue;
|
||||
}
|
||||
HydrologyPoint previous = centerline.get(pointIndex - 1);
|
||||
int steps = Math.max(Math.abs(current.x() - previous.x()), Math.abs(current.z() - previous.z()));
|
||||
for (int step = 1; step <= steps; step++) {
|
||||
double progress = step / (double) steps;
|
||||
HydrologyPoint cell = new HydrologyPoint(
|
||||
(int) StrictMath.round(previous.x() + (current.x() - previous.x()) * progress),
|
||||
(int) StrictMath.round(previous.y() + (current.y() - previous.y()) * progress),
|
||||
(int) StrictMath.round(previous.z() + (current.z() - previous.z()) * progress)
|
||||
);
|
||||
appendStation(points, segmentIndices, heads, cell, segmentIndex);
|
||||
}
|
||||
}
|
||||
if (!mouth) {
|
||||
exposedStations = points.size();
|
||||
}
|
||||
}
|
||||
return new Stations(points, segmentIndices.toIntArray(), heads.toIntArray(), exposedStations);
|
||||
}
|
||||
|
||||
private static void appendStation(
|
||||
ArrayList<HydrologyPoint> points,
|
||||
IntArrayList segmentIndices,
|
||||
IntArrayList heads,
|
||||
HydrologyPoint cell,
|
||||
int segmentIndex
|
||||
) {
|
||||
if (!points.isEmpty()) {
|
||||
HydrologyPoint last = points.getLast();
|
||||
if (last.x() == cell.x() && last.z() == cell.z()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
points.add(cell);
|
||||
segmentIndices.add(segmentIndex);
|
||||
heads.add(cell.y());
|
||||
}
|
||||
|
||||
private record Stations(List<HydrologyPoint> points, int[] segmentIndex, int[] head, int exposedStations) {
|
||||
private int count() {
|
||||
return points.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyTerrainSample;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public record SurfaceLayerColumn(
|
||||
int x,
|
||||
int z,
|
||||
HydrologyTerrainSample terrain,
|
||||
HydrologyColumnLayer layer,
|
||||
SurfaceRole role,
|
||||
boolean apron
|
||||
) {
|
||||
public SurfaceLayerColumn {
|
||||
Objects.requireNonNull(terrain, "terrain");
|
||||
Objects.requireNonNull(layer, "layer");
|
||||
Objects.requireNonNull(role, "role");
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydraulicSegment;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyHash;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyPlannerSettings;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyPoint;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class SurfaceSegmentLabeler {
|
||||
private static final long SEGMENT_SALT = 0x5345474d454e54L;
|
||||
|
||||
private SurfaceSegmentLabeler() {
|
||||
}
|
||||
|
||||
public static List<HydraulicSegment> label(
|
||||
long worldSeed,
|
||||
long courseId,
|
||||
int[] x,
|
||||
int[] z,
|
||||
int[] head,
|
||||
double[] width,
|
||||
double[] depth,
|
||||
HydrologyPlannerSettings.Banks banks
|
||||
) {
|
||||
int pairs = head.length - 1;
|
||||
if (pairs < 1) {
|
||||
return List.of();
|
||||
}
|
||||
ArrayList<Run> runs = new ArrayList<>();
|
||||
for (int pair = 0; pair < pairs; pair++) {
|
||||
HydrologyFeatureType type = pairType(head[pair] - head[pair + 1], banks.waterfallMinimumDrop());
|
||||
if (!runs.isEmpty() && runs.getLast().type == type) {
|
||||
runs.getLast().end = pair;
|
||||
} else {
|
||||
runs.add(new Run(pair, pair, type));
|
||||
}
|
||||
}
|
||||
for (Run run : runs) {
|
||||
if (run.type == HydrologyFeatureType.RIFFLE && run.pairs() >= 2) {
|
||||
run.type = HydrologyFeatureType.CASCADE;
|
||||
}
|
||||
}
|
||||
boolean merged = true;
|
||||
while (merged) {
|
||||
merged = false;
|
||||
for (int index = 1; index + 1 < runs.size(); index++) {
|
||||
Run previous = runs.get(index - 1);
|
||||
Run current = runs.get(index);
|
||||
Run next = runs.get(index + 1);
|
||||
if (current.type == HydrologyFeatureType.SURFACE_POOL
|
||||
&& current.pairs() < banks.cascadeRun() * 2
|
||||
&& isGraded(previous.type)
|
||||
&& isGraded(next.type)) {
|
||||
previous.end = next.end;
|
||||
previous.type = HydrologyFeatureType.CASCADE;
|
||||
runs.remove(index + 1);
|
||||
runs.remove(index);
|
||||
merged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!merged) {
|
||||
for (int index = 1; index < runs.size(); index++) {
|
||||
Run previous = runs.get(index - 1);
|
||||
Run current = runs.get(index);
|
||||
if (previous.type == current.type) {
|
||||
previous.end = current.end;
|
||||
runs.remove(index);
|
||||
merged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ArrayList<HydraulicSegment> segments = new ArrayList<>(runs.size());
|
||||
for (Run run : runs) {
|
||||
int startStation = run.start;
|
||||
int endStation = run.end + 1;
|
||||
ArrayList<HydrologyPoint> centerline = new ArrayList<>(endStation - startStation + 1);
|
||||
double totalWidth = 0D;
|
||||
double totalDepth = 0D;
|
||||
for (int station = startStation; station <= endStation; station++) {
|
||||
centerline.add(new HydrologyPoint(x[station], head[station], z[station]));
|
||||
totalWidth += width[station];
|
||||
totalDepth += depth[station];
|
||||
}
|
||||
int stations = endStation - startStation + 1;
|
||||
int segmentWidth = Math.max(1, (int) StrictMath.round(totalWidth / stations));
|
||||
int segmentDepth = Math.max(1, (int) StrictMath.round(totalDepth / stations));
|
||||
int index = segments.size();
|
||||
segments.add(new HydraulicSegment(
|
||||
HydrologyHash.mix(worldSeed, SEGMENT_SALT, courseId, index, run.type.ordinal()),
|
||||
courseId,
|
||||
run.type,
|
||||
head[startStation],
|
||||
head[endStation],
|
||||
segmentWidth,
|
||||
segmentDepth,
|
||||
false,
|
||||
false,
|
||||
centerline
|
||||
));
|
||||
}
|
||||
return List.copyOf(segments);
|
||||
}
|
||||
|
||||
private static HydrologyFeatureType pairType(int drop, int waterfallMinimumDrop) {
|
||||
if (drop <= 0) {
|
||||
return HydrologyFeatureType.SURFACE_POOL;
|
||||
}
|
||||
if (drop == 1) {
|
||||
return HydrologyFeatureType.RIFFLE;
|
||||
}
|
||||
return drop >= waterfallMinimumDrop ? HydrologyFeatureType.WATERFALL : HydrologyFeatureType.CASCADE;
|
||||
}
|
||||
|
||||
private static boolean isGraded(HydrologyFeatureType type) {
|
||||
return type == HydrologyFeatureType.RIFFLE || type == HydrologyFeatureType.CASCADE;
|
||||
}
|
||||
|
||||
private static final class Run {
|
||||
private final int start;
|
||||
private int end;
|
||||
private HydrologyFeatureType type;
|
||||
|
||||
private Run(int start, int end, HydrologyFeatureType type) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
private int pairs() {
|
||||
return end - start + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,10 @@ public record ValleyProfile(
|
||||
return rejection == null;
|
||||
}
|
||||
|
||||
public static ValleyProfile fromHeads(int[] head, int exposedStations) {
|
||||
return new ValleyProfile(head, new int[0], new int[0], new int[0], exposedStations, null);
|
||||
}
|
||||
|
||||
public static ValleyProfile rejected(HydrologyCandidateRejection rejection) {
|
||||
return new ValleyProfile(new int[0], new int[0], new int[0], new int[0], 0, rejection);
|
||||
}
|
||||
|
||||
+3
-3
@@ -22,7 +22,7 @@ public class ChannelProfileBuilderTest {
|
||||
@Test
|
||||
public void cruiseWidthIsConstantAndTapersFromTheHeadwater() {
|
||||
SurfaceCenterline centerline = straight(400);
|
||||
ChannelProfile profile = builder(1D, 1D).build(centerline, path(400), "water", false);
|
||||
ChannelProfile profile = builder(1D, 1D).build(centerline, "water", false);
|
||||
|
||||
assertEquals(400, profile.size());
|
||||
assertEquals(3D, profile.halfWidth()[200], 1.0E-9D);
|
||||
@@ -38,7 +38,7 @@ public class ChannelProfileBuilderTest {
|
||||
@Test
|
||||
public void directOceanCoursesFlareAtTheMouth() {
|
||||
SurfaceCenterline centerline = straight(400);
|
||||
ChannelProfile profile = builder(1D, 1D).build(centerline, path(400), "water", true);
|
||||
ChannelProfile profile = builder(1D, 1D).build(centerline, "water", true);
|
||||
|
||||
assertEquals(3D * 1.6D, profile.halfWidth()[399], 0.05D);
|
||||
assertEquals(3D, profile.halfWidth()[300], 1.0E-9D);
|
||||
@@ -47,7 +47,7 @@ public class ChannelProfileBuilderTest {
|
||||
@Test
|
||||
public void policyMultipliersScaleWidthDepthAndBank() {
|
||||
SurfaceCenterline centerline = straight(400);
|
||||
ChannelProfile profile = builder(2D, 0.5D).build(centerline, path(400), "water", false);
|
||||
ChannelProfile profile = builder(2D, 0.5D).build(centerline, "water", false);
|
||||
|
||||
assertEquals(6D, profile.halfWidth()[200], 1.0E-9D);
|
||||
assertEquals(1.5D, profile.depth()[200], 1.0E-9D);
|
||||
|
||||
+1
-1
@@ -158,7 +158,7 @@ public class ErosionFieldCompilerTest {
|
||||
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);
|
||||
.build(centerline, "water", terminal == SurfaceTerminal.OCEAN_MOUTH);
|
||||
ValleyProfile valley = new ValleyProfileSolver(surface, sampler, SEA_LEVEL, 64)
|
||||
.solve(centerline, channel, terminal, terminalHead);
|
||||
assertNull(valley.rejection());
|
||||
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydraulicSegment;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyCandidateRejection;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
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.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class SurfaceCourseBuilderTest {
|
||||
private static final int SEA_LEVEL = 60;
|
||||
private static final long COURSE_ID = 4242L;
|
||||
private static final HydrologyGeometrySampler CONSTANT_GEOMETRY = request -> switch (request.field()) {
|
||||
case SURFACE_WIDTH -> 6;
|
||||
case SURFACE_DEPTH -> 3;
|
||||
default -> request.minimum();
|
||||
};
|
||||
|
||||
@Test
|
||||
public void slopedCourseToTheSeaProducesNonRisingLabelledSegmentsEndingAtSeaLevel() {
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 260
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(110 - x / 6, 0D, "land");
|
||||
SurfaceCourseResult result = builder(sampler).build(
|
||||
7L,
|
||||
COURSE_ID,
|
||||
"water",
|
||||
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(140, 0, 0), new HydrologyPoint(280, 0, 0)),
|
||||
SurfaceTerminal.OCEAN_MOUTH,
|
||||
SEA_LEVEL
|
||||
);
|
||||
|
||||
assertNull(result.rejection());
|
||||
List<HydraulicSegment> segments = result.segments();
|
||||
assertTrue(segments.size() > 3);
|
||||
assertEquals(0, segments.getFirst().start().x());
|
||||
assertEquals(SEA_LEVEL, result.lastHead());
|
||||
assertEquals(SEA_LEVEL, segments.getLast().downstreamHeadY());
|
||||
int previous = Integer.MAX_VALUE;
|
||||
for (HydraulicSegment segment : segments) {
|
||||
assertEquals(COURSE_ID, segment.courseId());
|
||||
assertTrue(segment.upstreamHeadY() <= previous);
|
||||
assertTrue(segment.downstreamHeadY() <= segment.upstreamHeadY());
|
||||
assertTrue(segment.type().isSurface());
|
||||
previous = segment.downstreamHeadY();
|
||||
}
|
||||
for (int index = 1; index < segments.size(); index++) {
|
||||
HydraulicSegment before = segments.get(index - 1);
|
||||
HydraulicSegment after = segments.get(index);
|
||||
assertEquals(before.end().x(), after.start().x());
|
||||
assertEquals(before.end().z(), after.start().z());
|
||||
assertEquals(before.downstreamHeadY(), after.upstreamHeadY());
|
||||
}
|
||||
assertTrue(result.pathEnd().x() >= 250);
|
||||
assertTrue(result.lastWidth() >= 6);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void flatCourseIsOneSurfacePool() {
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(80, 0D, "land");
|
||||
SurfaceCourseResult result = builder(sampler).build(
|
||||
7L,
|
||||
COURSE_ID,
|
||||
"water",
|
||||
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)),
|
||||
SurfaceTerminal.SINKHOLE,
|
||||
40
|
||||
);
|
||||
|
||||
assertNull(result.rejection());
|
||||
assertEquals(1, result.segments().size());
|
||||
assertEquals(HydrologyFeatureType.SURFACE_POOL, result.segments().getFirst().type());
|
||||
assertEquals(79, result.lastHead());
|
||||
assertEquals(201, result.segments().getFirst().centerline().size());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void cliffProducesAWaterfallSegment() {
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(x < 100 ? 100 : 90, 0D, "land");
|
||||
SurfaceCourseResult result = builder(sampler).build(
|
||||
7L,
|
||||
COURSE_ID,
|
||||
"water",
|
||||
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)),
|
||||
SurfaceTerminal.SINKHOLE,
|
||||
40
|
||||
);
|
||||
|
||||
assertNull(result.rejection());
|
||||
assertEquals(3, result.segments().size());
|
||||
assertEquals(HydrologyFeatureType.SURFACE_POOL, result.segments().get(0).type());
|
||||
assertEquals(HydrologyFeatureType.WATERFALL, result.segments().get(1).type());
|
||||
assertEquals(10, result.segments().get(1).drop());
|
||||
assertEquals(HydrologyFeatureType.SURFACE_POOL, result.segments().get(2).type());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void gentleSlopeProducesRifflesAndSteepSlopeProducesCascades() {
|
||||
HydrologyTerrainSampler gentle = (int x, int z) -> HydrologyTerrainSample.openLand(120 - x / 10, 0D, "land");
|
||||
SurfaceCourseResult gentleResult = builder(gentle).build(7L, COURSE_ID, "water",
|
||||
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)), SurfaceTerminal.SINKHOLE, 40);
|
||||
HydrologyTerrainSampler steep = (int x, int z) -> HydrologyTerrainSample.openLand(300 - x, 0D, "land");
|
||||
SurfaceCourseResult steepResult = builder(steep).build(7L, COURSE_ID, "water",
|
||||
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)), SurfaceTerminal.SINKHOLE, 40);
|
||||
|
||||
assertNull(gentleResult.rejection());
|
||||
assertNull(steepResult.rejection());
|
||||
assertTrue(gentleResult.segments().stream().anyMatch(segment -> segment.type() == HydrologyFeatureType.RIFFLE));
|
||||
assertTrue(gentleResult.segments().stream().noneMatch(segment -> segment.type() == HydrologyFeatureType.CASCADE));
|
||||
assertTrue(steepResult.segments().stream().anyMatch(segment -> segment.type() == HydrologyFeatureType.CASCADE));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void submergedCourseIsRejected() {
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> HydrologyTerrainSample.openLand(50, 0D, "land");
|
||||
SurfaceCourseResult result = builder(sampler).build(
|
||||
7L,
|
||||
COURSE_ID,
|
||||
"water",
|
||||
List.of(new HydrologyPoint(0, 0, 0), new HydrologyPoint(200, 0, 0)),
|
||||
SurfaceTerminal.OCEAN_MOUTH,
|
||||
SEA_LEVEL
|
||||
);
|
||||
|
||||
assertEquals(HydrologyCandidateRejection.COURSE_TOO_SHORT, result.rejection());
|
||||
assertTrue(result.segments().isEmpty());
|
||||
}
|
||||
|
||||
private static SurfaceCourseBuilder builder(HydrologyTerrainSampler sampler) {
|
||||
return new SurfaceCourseBuilder(HydrologyPlannerSettings.defaults().surface(), sampler, CONSTANT_GEOMETRY, SEA_LEVEL, 64);
|
||||
}
|
||||
}
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
package art.arcane.iris.engine.hydrology.surface;
|
||||
|
||||
import art.arcane.iris.engine.hydrology.HydraulicSegment;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyColumnLayer;
|
||||
import art.arcane.iris.engine.hydrology.HydrologyFeatureType;
|
||||
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.RiverCourse;
|
||||
import art.arcane.iris.engine.hydrology.RiverCourseType;
|
||||
import art.arcane.iris.engine.hydrology.RiverFootprint;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class SurfaceFootprintCompilerTest {
|
||||
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 compiledLayersDescribeAContainedCarveOnlyChannelWithShoreAndBank() {
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 150 && z > 8
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(80 + Math.max(0, -z) / 2, 0D, "land");
|
||||
RiverCourse course = course(List.of(
|
||||
segment(1L, HydrologyFeatureType.SURFACE_POOL, 79, 79, points(0, 60, 79)),
|
||||
segment(2L, HydrologyFeatureType.RIFFLE, 79, 78, List.of(new HydrologyPoint(60, 79, 0), new HydrologyPoint(61, 78, 0))),
|
||||
segment(3L, HydrologyFeatureType.SURFACE_POOL, 78, 78, points(61, 140, 78))
|
||||
));
|
||||
SurfaceFootprint footprint = compiler(sampler).compile(course);
|
||||
|
||||
assertEquals(0, footprint.uncontainedWetCells());
|
||||
assertFalse(footprint.columns().isEmpty());
|
||||
HashSet<Long> keys = new HashSet<>();
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
assertTrue(keys.add(RiverFootprint.pack(column.x(), column.z())));
|
||||
HydrologyColumnLayer layer = column.layer();
|
||||
assertTrue(layer.bedY() <= column.terrain().naturalHeight());
|
||||
assertEquals(layer.fluidHeadY(), layer.ceilingY());
|
||||
assertEquals(1L, layer.feature().courseId());
|
||||
assertTrue(layer.feature().type().isSurface());
|
||||
if (layer.channel()) {
|
||||
assertTrue(layer.fluidOwned());
|
||||
assertTrue(layer.connectedFluid());
|
||||
assertTrue(layer.terrainOwned());
|
||||
assertTrue(layer.fluidHeadY() < column.terrain().naturalHeight());
|
||||
assertTrue(layer.bedY() < layer.fluidHeadY());
|
||||
} else {
|
||||
assertTrue(layer.grading());
|
||||
assertTrue(layer.terrainOwned());
|
||||
assertFalse(layer.fluidOwned());
|
||||
assertEquals(layer.bedY(), layer.fluidHeadY());
|
||||
}
|
||||
}
|
||||
SurfaceLayerColumn center = column(footprint, 30, 0);
|
||||
assertNotNull(center);
|
||||
assertTrue(center.layer().channel());
|
||||
assertEquals(79, center.layer().fluidHeadY());
|
||||
assertTrue(center.layer().feature().source() == false);
|
||||
SurfaceLayerColumn source = column(footprint, 0, 0);
|
||||
assertNotNull(source);
|
||||
assertTrue(source.layer().feature().source());
|
||||
SurfaceLayerColumn shore = column(footprint, 30, 4);
|
||||
assertNotNull(shore);
|
||||
assertTrue(shore.layer().shore());
|
||||
assertEquals("land", shore.layer().shoreBiomeKey());
|
||||
SurfaceLayerColumn bank = column(footprint, 30, -8);
|
||||
assertNotNull(bank);
|
||||
assertTrue(bank.layer().grading());
|
||||
assertFalse(bank.layer().shore());
|
||||
assertNull(column(footprint, 160, 12));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void mouthOverTheOceanPublishesOnlyANonOwningApron() {
|
||||
HydrologyTerrainSampler sampler = (int x, int z) -> x >= 100
|
||||
? HydrologyTerrainSample.ocean(50, "ocean")
|
||||
: HydrologyTerrainSample.openLand(70, 0D, "land");
|
||||
RiverCourse course = course(List.of(
|
||||
segment(1L, HydrologyFeatureType.SURFACE_POOL, 69, 69, points(0, 96, 69)),
|
||||
segment(2L, HydrologyFeatureType.WATERFALL, 69, SEA_LEVEL, List.of(new HydrologyPoint(96, 69, 0), new HydrologyPoint(97, SEA_LEVEL, 0))),
|
||||
segment(3L, HydrologyFeatureType.MOUTH, SEA_LEVEL, SEA_LEVEL, points(97, 112, SEA_LEVEL))
|
||||
));
|
||||
SurfaceFootprint footprint = compiler(sampler).compile(course);
|
||||
|
||||
int aprons = 0;
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
if (column.terrain().ocean()) {
|
||||
assertTrue(column.layer().oceanApron());
|
||||
assertFalse(column.layer().terrainOwned());
|
||||
assertFalse(column.layer().fluidOwned());
|
||||
assertEquals(HydrologyFeatureType.MOUTH, column.layer().feature().type());
|
||||
aprons++;
|
||||
} else {
|
||||
assertFalse(column.layer().oceanApron());
|
||||
}
|
||||
}
|
||||
assertTrue(aprons > 0);
|
||||
assertNull(column(footprint, 111, 0));
|
||||
}
|
||||
|
||||
private static SurfaceLayerColumn column(SurfaceFootprint footprint, int x, int z) {
|
||||
for (SurfaceLayerColumn column : footprint.columns()) {
|
||||
if (column.x() == x && column.z() == z) {
|
||||
return column;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static SurfaceFootprintCompiler compiler(HydrologyTerrainSampler sampler) {
|
||||
return new SurfaceFootprintCompiler(HydrologyPlannerSettings.defaults(), sampler, CONSTANT_GEOMETRY);
|
||||
}
|
||||
|
||||
private static RiverCourse course(List<HydraulicSegment> segments) {
|
||||
return new RiverCourse(1L, RiverCourseType.SURFACE, OptionalLong.of(9L), OptionalLong.of(8L), "water", 1, List.of(), segments);
|
||||
}
|
||||
|
||||
private static HydraulicSegment segment(long id, HydrologyFeatureType type, int upstream, int downstream, List<HydrologyPoint> centerline) {
|
||||
return new HydraulicSegment(id, 1L, type, upstream, downstream, 6, 3, false, false, centerline);
|
||||
}
|
||||
|
||||
private static List<HydrologyPoint> points(int fromX, int toX, int y) {
|
||||
ArrayList<HydrologyPoint> points = new ArrayList<>();
|
||||
for (int x = fromX; x <= toX; x++) {
|
||||
points.add(new HydrologyPoint(x, y, 0));
|
||||
}
|
||||
return points;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -162,7 +162,7 @@ public class ValleyProfileSolverTest {
|
||||
|
||||
private static ChannelProfile channel(int stations, HydrologyTerrainSampler sampler) {
|
||||
return new ChannelProfileBuilder(HydrologyPlannerSettings.defaults().surface(), sampler, CONSTANT_GEOMETRY)
|
||||
.build(straight(stations), path(stations), "water", false);
|
||||
.build(straight(stations), "water", false);
|
||||
}
|
||||
|
||||
private static SurfaceCenterline straight(int stations) {
|
||||
|
||||
Reference in New Issue
Block a user