Keep coarser ground in 3D views until new backdrop arrives

- Backdrop not sampled yet borrows the nearest sampled tile up to four levels coarser, so zooming in leaves no gap
This commit is contained in:
piexel committed 2026-10-25 16:43:27 +01:00
1 parent ff6668a48b
commit 54213d4b62
3 files changed
+152 -2

No files matched your search

@@ -21,7 +21,11 @@ package art.arcane.iris.studio.map;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set; import java.util.Set;
/** /**
@@ -30,7 +34,7 @@ import java.util.Set;
* whole view and more right away. Preview tiles one level coarser than detail come between them: a quarter of the * whole view and more right away. Preview tiles one level coarser than detail come between them: a quarter of the
* columns, so the whole view looks nearly finished long before the detail is in. Until it is, coarser tiles stand in * columns, so the whole view looks nearly finished long before the detail is in. Until it is, coarser tiles stand in
* for it, with holes where finer tiles are ready; detail left over from before zooming out stands in too, as does * for it, with holes where finer tiles are ready; detail left over from before zooming out stands in too, as does
* ground from before the noise changed. * ground from before the noise changed, and backdrop left over from before zooming in.
*/ */
final class TerrainCover { final class TerrainCover {
private static final double MOST_COLUMN_PIXELS = 1D; private static final double MOST_COLUMN_PIXELS = 1D;
@@ -96,9 +100,57 @@ final class TerrainCover {
} }
private void coverSight() { private void coverSight() {
Map<TerrainTile, List<TerrainTile>> coveredByBackdrop = new LinkedHashMap<>();
for (TerrainTile backdrop : sight.tilesInSight(backdropLevel, BACKDROP_MARGIN_SHARE)) { for (TerrainTile backdrop : sight.tilesInSight(backdropLevel, BACKDROP_MARGIN_SHARE)) {
cover(backdrop, true); coveredByBackdrop.put(backdrop, cover(backdrop, true));
} }
coverMissingBackdrop(coveredByBackdrop);
}
/**
* Backdrop that isn't sampled yet, as right after zooming in, borrows the nearest coarser tile that is, with holes
* where the backdrop and finer ground around it are ready. Only as far up as holes can still be cut.
*/
private void coverMissingBackdrop(Map<TerrainTile, List<TerrainTile>> coveredByBackdrop) {
Set<TerrainTile> standIns = new LinkedHashSet<>();
coveredByBackdrop.forEach((backdrop, covered) -> {
if (!covered.equals(List.of(backdrop))) {
nearestSampledAncestor(backdrop).ifPresent(standIns::add);
}
});
for (TerrainTile standIn : standIns) {
TerrainLayer layer = tiles.layerOf(standIn);
if (layer != null) {
draw(standIn, layer, fillersOf(standIn, coveredByBackdrop, standIns));
}
}
}
private Optional<TerrainTile> nearestSampledAncestor(TerrainTile backdrop) {
for (int levelsUp = 1; levelsUp <= TerrainHole.MOST_LEVELS_FINER; levelsUp++) {
TerrainTile ancestor = backdrop.ancestorAt(backdrop.level() + levelsUp);
if (tiles.isSampled(ancestor)) {
return Optional.of(ancestor);
}
}
return Optional.empty();
}
// Everything drawn inside the stand-in: the backdrop's ready ground, and finer stand-ins.
private static List<TerrainTile> fillersOf(TerrainTile standIn, Map<TerrainTile, List<TerrainTile>> coveredByBackdrop,
Set<TerrainTile> standIns) {
List<TerrainTile> fillers = new ArrayList<>();
coveredByBackdrop.forEach((backdrop, covered) -> {
if (backdrop.ancestorAt(standIn.level()).equals(standIn)) {
fillers.addAll(covered);
}
});
for (TerrainTile finer : standIns) {
if (finer.level() < standIn.level() && finer.ancestorAt(standIn.level()).equals(standIn)) {
fillers.add(finer);
}
}
return fillers;
} }
/** /**
@@ -66,6 +66,14 @@ record TerrainTile(int level, int tileX, int tileZ) {
new TerrainTile(finer, childX, childZ + 1), new TerrainTile(finer, childX + 1, childZ + 1)); new TerrainTile(finer, childX, childZ + 1), new TerrainTile(finer, childX + 1, childZ + 1));
} }
/**
* The tile of a coarser level that this one lies inside.
*/
TerrainTile ancestorAt(int coarserLevel) {
int levelsUp = coarserLevel - level;
return new TerrainTile(coarserLevel, tileX >> levelsUp, tileZ >> levelsUp);
}
/** /**
* This tile's columns that a finer tile inside it covers. * This tile's columns that a finer tile inside it covers.
*/ */
@@ -0,0 +1,90 @@
package art.arcane.iris.studio.map;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
public final class TerrainCoverTest {
private static final int WIDTH = 1200;
private static final int HEIGHT = 800;
private static final TerrainSampling FLAT = new TerrainSampling((grid, row, heights, colors) -> {
}, 0, 64, Double.NaN);
private static final OrbitProjection PROJECTION = NoiseTerrainScene.projectionFrom(Vec3.ZERO, NoiseTerrainScene.START,
WIDTH, HEIGHT, 0);
@Test
public void backdropNotSampledYetBorrowsTheNearestSampledCoarserTile() {
TerrainTile coarser = TerrainTile.containing(0D, 0D, backdropLevel() + 1);
List<TerrainTile> drawn = drawnTiles(Map.of(coarser, flatLayer(coarser)));
assertTrue(drawn.contains(coarser));
}
@Test
public void tilesTooCoarseToCutHolesInDoNotStandIn() {
TerrainTile tooCoarse = TerrainTile.containing(0D, 0D, backdropLevel() + TerrainHole.MOST_LEVELS_FINER + 1);
List<TerrainTile> drawn = drawnTiles(Map.of(tooCoarse, flatLayer(tooCoarse)));
assertFalse(drawn.contains(tooCoarse));
}
private static int backdropLevel() {
TerrainCover.Result nothingSampled = TerrainCover.of(tilesWith(Map.of()), PROJECTION, WIDTH, HEIGHT, 0);
int detailLevel = Integer.numberOfTrailingZeros(nothingSampled.detailStep());
return detailLevel + 3;
}
private static List<TerrainTile> drawnTiles(Map<TerrainTile, TerrainLayer> sampled) {
return TerrainCover.of(tilesWith(sampled), PROJECTION, WIDTH, HEIGHT, 0).layers().stream()
.map(HoledLayer::tile)
.toList();
}
private static TerrainTiles tilesWith(Map<TerrainTile, TerrainLayer> sampled) {
return new TerrainTiles(FLAT, new NeverRuns(), 1, () -> {
}, sampled);
}
private static TerrainLayer flatLayer(TerrainTile tile) {
return TerrainLayer.of(NoiseTerrain.empty(tile.grid()), FLAT.floorY());
}
// Keeps the wanted tiles unsampled, so each test decides exactly what is ready.
private static final class NeverRuns extends AbstractExecutorService {
@Override
public void execute(Runnable command) {
}
@Override
public void shutdown() {
}
@Override
public List<Runnable> shutdownNow() {
return List.of();
}
@Override
public boolean isShutdown() {
return false;
}
@Override
public boolean isTerminated() {
return false;
}
@Override
public boolean awaitTermination(long timeout, TimeUnit unit) {
return true;
}
}
}