mirror of
https://github.com/VolmitSoftware/Iris.git
synced 2026-11-01 08:50:06 +00:00
Indexed raster channel lookups
- Indexed reach segments into a channel grid around each rasterized chunk - Skipped sea inlet column checks outside each inlet's bounds - Returned the empty raster for chunks with no water features in reach - Added a test that covered channel lookups match the full segment scan
This commit is contained in:
1 parent
a879985875
commit
0c7adc5da4
4 files changed
+245
-9
No files matched your search
@@ -12,6 +12,7 @@ import art.arcane.iris.generation.hydrology.water.PondMask;
|
||||
import art.arcane.iris.generation.hydrology.water.Reach;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@@ -28,17 +29,20 @@ final class ChunkFeatures {
|
||||
private final List<Pond> ponds;
|
||||
private final List<Pond> lakes;
|
||||
private final List<SeaInlet> inlets;
|
||||
private final int[] inletBoxes;
|
||||
private final int[] lakeBoxes;
|
||||
private final ReachSegments segments = new ReachSegments(SEGMENT_CAPACITY);
|
||||
private final MaskEdges pondShores = new MaskEdges(MaskEdges.Kind.WATER);
|
||||
private final MaskEdges pondBanks = new MaskEdges(MaskEdges.Kind.BANK);
|
||||
private final MaskEdges lakeReach = new MaskEdges(MaskEdges.Kind.GROWN_WATER);
|
||||
private boolean inert;
|
||||
|
||||
private ChunkFeatures(List<Reach> reaches, List<Pond> ponds, List<Pond> lakes, List<SeaInlet> inlets) {
|
||||
private ChunkFeatures(List<Reach> reaches, List<Pond> ponds, List<Pond> lakes, List<SeaInlet> inlets, int[] inletBoxes) {
|
||||
this.reaches = reaches;
|
||||
this.ponds = ponds;
|
||||
this.lakes = lakes;
|
||||
this.inlets = inlets;
|
||||
this.inletBoxes = inletBoxes;
|
||||
this.lakeBoxes = new int[4 * lakes.size()];
|
||||
}
|
||||
|
||||
@@ -48,12 +52,15 @@ final class ChunkFeatures {
|
||||
List<Reach> reaches = new ArrayList<>();
|
||||
List<Pond> ponds = new ArrayList<>();
|
||||
List<SeaInlet> inlets = new ArrayList<>();
|
||||
int[] inletBoxes = new int[0];
|
||||
for (IndexedWindow window : windows) {
|
||||
for (SeaInlet inlet : window.plan().inlets()) {
|
||||
int[] bounds = SeaInletRasterizer.bounds(inlet);
|
||||
int padding = RiffleProfile.RADIUS + 1;
|
||||
if (bounds[0] <= chunkX * CHUNK + CHUNK - 1 + padding && bounds[2] >= chunkX * CHUNK - padding
|
||||
&& bounds[1] <= chunkZ * CHUNK + CHUNK - 1 + padding && bounds[3] >= chunkZ * CHUNK - padding) {
|
||||
inletBoxes = Arrays.copyOf(inletBoxes, inletBoxes.length + 4);
|
||||
System.arraycopy(bounds, 0, inletBoxes, 4 * inlets.size(), 4);
|
||||
inlets.add(inlet);
|
||||
}
|
||||
}
|
||||
@@ -64,17 +71,22 @@ final class ChunkFeatures {
|
||||
}
|
||||
}
|
||||
List<Pond> lakes = lakes(chunkX, chunkZ, halo, lakeRegionPlans, lakeRegions);
|
||||
ChunkFeatures features = new ChunkFeatures(reaches, ponds, lakes, inlets);
|
||||
ChunkFeatures features = new ChunkFeatures(reaches, ponds, lakes, inlets, inletBoxes);
|
||||
features.index(settings, chunkX, chunkZ);
|
||||
return features;
|
||||
}
|
||||
|
||||
boolean empty() {
|
||||
return reaches.isEmpty() && ponds.isEmpty() && lakes.isEmpty() && inlets.isEmpty();
|
||||
boolean inert() {
|
||||
return inert;
|
||||
}
|
||||
|
||||
SeaInlet openInletAt(int x, int z, int[] column) {
|
||||
for (SeaInlet inlet : inlets) {
|
||||
for (int index = 0; index < inlets.size(); index++) {
|
||||
int box = 4 * index;
|
||||
if (x < inletBoxes[box] || z < inletBoxes[box + 1] || x > inletBoxes[box + 2] || z > inletBoxes[box + 3]) {
|
||||
continue;
|
||||
}
|
||||
SeaInlet inlet = inlets.get(index);
|
||||
if (SeaInletRasterizer.column(inlet, x, z, column) && column[1] == SeaInlet.OPEN) {
|
||||
return inlet;
|
||||
}
|
||||
@@ -179,6 +191,8 @@ final class ChunkFeatures {
|
||||
for (Reach reach : reaches) {
|
||||
segments.addNear(reach, minX - 1f, minZ - 1f, maxX + 1f, maxZ + 1f, outward);
|
||||
}
|
||||
int riffle = RiffleProfile.RADIUS + 1;
|
||||
segments.coverChannels(minX - riffle, minZ - riffle, maxX + riffle, maxZ + riffle);
|
||||
HydrologySettings.PondShore shore = settings.ponds().shore();
|
||||
int reach = Math.max(shore.taper(), ChunkWaterRaster.MAX_SHORE_DISTANCE) + 1;
|
||||
int shelf = shore.shelf() + 1;
|
||||
@@ -192,6 +206,39 @@ final class ChunkFeatures {
|
||||
lakeReach.collect(mask, minX - shoreReach, minZ - shoreReach, maxX + shoreReach, maxZ + shoreReach);
|
||||
System.arraycopy(grownBox(mask), 0, lakeBoxes, 4 * index, 4);
|
||||
}
|
||||
inert = segments.isEmpty() && !inletsTouch(minX - 1, minZ - 1, maxX + 1, maxZ + 1)
|
||||
&& !pondsTouch(minX - 1 - reach, minZ - 1 - reach, maxX + 1 + reach, maxZ + 1 + reach)
|
||||
&& !lakesTouch(minX - 2 - shoreReach, minZ - 2 - shoreReach, maxX + 2 + shoreReach, maxZ + 2 + shoreReach);
|
||||
}
|
||||
|
||||
private boolean inletsTouch(int minX, int minZ, int maxX, int maxZ) {
|
||||
for (int box = 0; box < inletBoxes.length; box += 4) {
|
||||
if (inletBoxes[box] <= maxX && inletBoxes[box + 2] >= minX && inletBoxes[box + 1] <= maxZ && inletBoxes[box + 3] >= minZ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean pondsTouch(int minX, int minZ, int maxX, int maxZ) {
|
||||
for (Pond pond : ponds) {
|
||||
PondMask mask = pond.mask();
|
||||
int fromX = mask.originX() - (mask.step() >> 1);
|
||||
int fromZ = mask.originZ() - (mask.step() >> 1);
|
||||
if (fromX <= maxX && fromX + mask.width() * mask.step() > minX && fromZ <= maxZ && fromZ + mask.depth() * mask.step() > minZ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean lakesTouch(int minX, int minZ, int maxX, int maxZ) {
|
||||
for (int box = 0; box < lakeBoxes.length; box += 4) {
|
||||
if (lakeBoxes[box] <= maxX && lakeBoxes[box + 2] > minX && lakeBoxes[box + 1] <= maxZ && lakeBoxes[box + 3] > minZ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Map<WindowKey, IndexedWindow> windowsByKey(int chunkX, int chunkZ, List<IndexedWindow> windows) {
|
||||
|
||||
@@ -37,7 +37,7 @@ public final class ChunkRasterizer {
|
||||
List<RegionPlan> lakeRegionPlans) {
|
||||
Objects.requireNonNull(naturalHeight, "naturalHeight");
|
||||
ChunkFeatures features = ChunkFeatures.gather(settings, chunkX, chunkZ, halo, windows, lakeRegionPlans, lakeRegions(chunkX, chunkZ));
|
||||
if (features.empty()) {
|
||||
if (features.inert()) {
|
||||
return ChunkWaterRaster.EMPTY;
|
||||
}
|
||||
return new RasterComposer(settings, features, chunkX, chunkZ).compose(naturalHeight);
|
||||
|
||||
@@ -6,6 +6,8 @@ import java.util.Arrays;
|
||||
|
||||
final class ReachSegments {
|
||||
private static final int RUN = 16;
|
||||
private static final int COVER_SHIFT = 2;
|
||||
private static final float COVER_MARGIN = 1f;
|
||||
|
||||
private Reach[] reaches;
|
||||
private int[] segments;
|
||||
@@ -24,6 +26,13 @@ final class ReachSegments {
|
||||
private int[] runBands = new int[8];
|
||||
private int runs;
|
||||
private boolean sealed;
|
||||
private boolean covered;
|
||||
private int coverX;
|
||||
private int coverZ;
|
||||
private int coverWidth;
|
||||
private int coverDepth;
|
||||
private int[] coverStarts;
|
||||
private int[] coverEntries;
|
||||
|
||||
ReachSegments(int capacity) {
|
||||
int initial = Math.max(1, capacity);
|
||||
@@ -71,6 +80,19 @@ final class ReachSegments {
|
||||
}
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return size == 0;
|
||||
}
|
||||
|
||||
void coverChannels(int minX, int minZ, int maxX, int maxZ) {
|
||||
covered = true;
|
||||
coverX = minX;
|
||||
coverZ = minZ;
|
||||
coverWidth = ((maxX - minX) >> COVER_SHIFT) + 1;
|
||||
coverDepth = ((maxZ - minZ) >> COVER_SHIFT) + 1;
|
||||
sealed = false;
|
||||
}
|
||||
|
||||
void nearest(double x, double z, Scope scope, ReachCursor cursor) {
|
||||
if (!sealed) {
|
||||
seal();
|
||||
@@ -89,6 +111,16 @@ final class ReachSegments {
|
||||
hint(channelHint, x, z, unbounded, cursor);
|
||||
hint(bankHint, x, z, unbounded, cursor);
|
||||
hint(edgeHint, x, z, unbounded, cursor);
|
||||
int cell = scope == Scope.CHANNEL ? coveredCell(x, z) : -1;
|
||||
if (cell >= 0) {
|
||||
for (int entry = coverStarts[cell]; entry < coverStarts[cell + 1]; entry++) {
|
||||
int index = coverEntries[entry];
|
||||
if (index != channelHint && index != bankHint && index != edgeHint && !beyondSegment(index, x, z, scope, cursor)) {
|
||||
evaluate(index, x, z, false, cursor);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
int first = 0;
|
||||
for (int run = 0; run < runs; run++) {
|
||||
int end = runEnds[run];
|
||||
@@ -99,9 +131,7 @@ final class ReachSegments {
|
||||
continue;
|
||||
}
|
||||
for (int index = first; index < end; index++) {
|
||||
if (!beyond(x, z, Math.min(startX[index], endX[index]), Math.min(startZ[index], endZ[index]), Math.max(startX[index], endX[index]),
|
||||
Math.max(startZ[index], endZ[index]), limit(radius[index], bands[index], scope, cursor))
|
||||
&& index != channelHint && index != bankHint && index != edgeHint) {
|
||||
if (!beyondSegment(index, x, z, scope, cursor) && index != channelHint && index != bankHint && index != edgeHint) {
|
||||
evaluate(index, x, z, unbounded, cursor);
|
||||
}
|
||||
}
|
||||
@@ -109,6 +139,23 @@ final class ReachSegments {
|
||||
}
|
||||
}
|
||||
|
||||
private int coveredCell(double x, double z) {
|
||||
if (coverStarts == null) {
|
||||
return -1;
|
||||
}
|
||||
int cellX = ((int) Math.floor(x) - coverX) >> COVER_SHIFT;
|
||||
int cellZ = ((int) Math.floor(z) - coverZ) >> COVER_SHIFT;
|
||||
if (cellX < 0 || cellZ < 0 || cellX >= coverWidth || cellZ >= coverDepth) {
|
||||
return -1;
|
||||
}
|
||||
return cellZ * coverWidth + cellX;
|
||||
}
|
||||
|
||||
private boolean beyondSegment(int index, double x, double z, Scope scope, ReachCursor cursor) {
|
||||
return beyond(x, z, Math.min(startX[index], endX[index]), Math.min(startZ[index], endZ[index]), Math.max(startX[index], endX[index]),
|
||||
Math.max(startZ[index], endZ[index]), limit(radius[index], bands[index], scope, cursor));
|
||||
}
|
||||
|
||||
private void hint(int index, double x, double z, boolean unbounded, ReachCursor cursor) {
|
||||
if (index >= 0 && index < size) {
|
||||
evaluate(index, x, z, unbounded, cursor);
|
||||
@@ -171,9 +218,56 @@ final class ReachSegments {
|
||||
addRun(start, end);
|
||||
start = end;
|
||||
}
|
||||
if (covered) {
|
||||
cover();
|
||||
}
|
||||
sealed = true;
|
||||
}
|
||||
|
||||
private void cover() {
|
||||
int cells = coverWidth * coverDepth;
|
||||
int[] starts = new int[cells + 1];
|
||||
int[] range = new int[4];
|
||||
for (int index = 0; index < size; index++) {
|
||||
if (coverRange(index, range)) {
|
||||
for (int cellZ = range[1]; cellZ <= range[3]; cellZ++) {
|
||||
for (int cellX = range[0]; cellX <= range[2]; cellX++) {
|
||||
starts[cellZ * coverWidth + cellX + 1]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int cell = 0; cell < cells; cell++) {
|
||||
starts[cell + 1] += starts[cell];
|
||||
}
|
||||
int[] entries = new int[starts[cells]];
|
||||
int[] fill = Arrays.copyOf(starts, cells);
|
||||
for (int index = 0; index < size; index++) {
|
||||
if (coverRange(index, range)) {
|
||||
for (int cellZ = range[1]; cellZ <= range[3]; cellZ++) {
|
||||
for (int cellX = range[0]; cellX <= range[2]; cellX++) {
|
||||
entries[fill[cellZ * coverWidth + cellX]++] = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
coverStarts = starts;
|
||||
coverEntries = entries;
|
||||
}
|
||||
|
||||
private boolean coverRange(int index, int[] range) {
|
||||
float reach = radius[index] + COVER_MARGIN;
|
||||
range[0] = Math.max(0, coverCell(Math.min(startX[index], endX[index]) - reach, coverX));
|
||||
range[1] = Math.max(0, coverCell(Math.min(startZ[index], endZ[index]) - reach, coverZ));
|
||||
range[2] = Math.min(coverWidth - 1, coverCell(Math.max(startX[index], endX[index]) + reach, coverX));
|
||||
range[3] = Math.min(coverDepth - 1, coverCell(Math.max(startZ[index], endZ[index]) + reach, coverZ));
|
||||
return range[0] <= range[2] && range[1] <= range[3];
|
||||
}
|
||||
|
||||
private static int coverCell(float block, int origin) {
|
||||
return ((int) Math.floor(block) - origin) >> COVER_SHIFT;
|
||||
}
|
||||
|
||||
private void addRun(int start, int end) {
|
||||
if (runs == runEnds.length) {
|
||||
runEnds = Arrays.copyOf(runEnds, 2 * runs);
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
package art.arcane.iris.generation.hydrology.raster;
|
||||
|
||||
import art.arcane.iris.generation.hydrology.config.IrisBankStyle;
|
||||
import art.arcane.iris.generation.hydrology.config.IrisHydrologyMaterials;
|
||||
import art.arcane.iris.generation.hydrology.water.HydrologyTier;
|
||||
import art.arcane.iris.generation.hydrology.water.Reach;
|
||||
import art.arcane.iris.generation.hydrology.water.ReachStyle;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.SplittableRandom;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class ReachSegmentsTest {
|
||||
private static final HydrologyTier TIER = new HydrologyTier("test", 0f, 1f, 4f, 1f, 0.4f, 1, true, 6, 4, IrisBankStyle.SMOOTH,
|
||||
false, new IrisHydrologyMaterials(), "water", List.of());
|
||||
|
||||
@Test
|
||||
public void coveredChannelLookupsMatchTheFullScan() {
|
||||
SplittableRandom random = new SplittableRandom(1337L);
|
||||
int hits = 0;
|
||||
for (int trial = 0; trial < 40; trial++) {
|
||||
int originX = random.nextInt(-2000, 2000) << 4;
|
||||
int originZ = random.nextInt(-2000, 2000) << 4;
|
||||
List<Reach> reaches = new ArrayList<>();
|
||||
int count = 1 + random.nextInt(12);
|
||||
for (int walk = 0; walk < count; walk++) {
|
||||
reaches.add(randomWalk(trial * 100L + walk, originX + 8, originZ + 8, random));
|
||||
}
|
||||
ReachSegments scanned = segments(reaches, originX, originZ);
|
||||
ReachSegments covered = segments(reaches, originX, originZ);
|
||||
covered.coverChannels(originX - 17, originZ - 17, originX + 32, originZ + 32);
|
||||
ReachCursor scan = new ReachCursor();
|
||||
ReachCursor cover = new ReachCursor();
|
||||
for (int z = originZ - 20; z <= originZ + 35; z++) {
|
||||
for (int x = originX - 20; x <= originX + 35; x++) {
|
||||
ReachSegments.Scope scope = random.nextInt(4) == 0 ? ReachSegments.Scope.SHORE : ReachSegments.Scope.CHANNEL;
|
||||
scanned.nearest(x, z, scope, scan);
|
||||
covered.nearest(x, z, scope, cover);
|
||||
String at = scope + " at " + x + "," + z;
|
||||
assertSameChoice(at + " channel", scan.channel, cover.channel);
|
||||
assertSameChoice(at + " bank", scan.bank, cover.bank);
|
||||
assertSameChoice(at + " edge", scan.edge, cover.edge);
|
||||
if (scan.channel.reach != null) {
|
||||
hits++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assertTrue("random walks cross the covered area, hits=" + hits, hits > 1000);
|
||||
}
|
||||
|
||||
private static void assertSameChoice(String what, ReachCursor.Choice expected, ReachCursor.Choice actual) {
|
||||
assertSame(what, expected.reach, actual.reach);
|
||||
assertEquals(what, expected.segment, actual.segment);
|
||||
assertEquals(what, expected.index, actual.index);
|
||||
assertEquals(what, Double.doubleToLongBits(expected.t), Double.doubleToLongBits(actual.t));
|
||||
assertEquals(what, Double.doubleToLongBits(expected.distance), Double.doubleToLongBits(actual.distance));
|
||||
}
|
||||
|
||||
private static ReachSegments segments(List<Reach> reaches, int originX, int originZ) {
|
||||
ReachSegments segments = new ReachSegments(16);
|
||||
for (Reach reach : reaches) {
|
||||
segments.addNear(reach, originX - 1f, originZ - 1f, originX + 16f, originZ + 16f, ChunkWaterRaster.MAX_SHORE_DISTANCE + 1f);
|
||||
}
|
||||
return segments;
|
||||
}
|
||||
|
||||
private static Reach randomWalk(long id, int centerX, int centerZ, SplittableRandom random) {
|
||||
int points = 2 + random.nextInt(30);
|
||||
float[] xs = new float[points];
|
||||
float[] zs = new float[points];
|
||||
float[] halfWidth = new float[points];
|
||||
xs[0] = centerX + (float) random.nextDouble(-60D, 60D);
|
||||
zs[0] = centerZ + (float) random.nextDouble(-60D, 60D);
|
||||
halfWidth[0] = (float) random.nextDouble(0.5D, 9D);
|
||||
for (int point = 1; point < points; point++) {
|
||||
xs[point] = xs[point - 1] + (float) random.nextDouble(-12D, 12D);
|
||||
zs[point] = zs[point - 1] + (float) random.nextDouble(-12D, 12D);
|
||||
halfWidth[point] = random.nextInt(8) == 0 ? halfWidth[point - 1] : (float) random.nextDouble(0.5D, 9D);
|
||||
}
|
||||
int[] grade = new int[points];
|
||||
float[] depth = new float[points];
|
||||
ReachStyle[] style = new ReachStyle[points];
|
||||
Arrays.fill(grade, 64);
|
||||
Arrays.fill(depth, 1f);
|
||||
Arrays.fill(style, ReachStyle.RUN);
|
||||
return new Reach(id, TIER, -1L, xs, zs, grade, halfWidth, depth, style, 100f);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user